Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"langchain": "^1.2.34",
"lru-cache": "^11.2.7",
"nanoid": "5.1.7",
"nodemailer": "^8.0.2",
"nodemailer": "^8.0.4",
"nunjucks": "^3.2.4",
"openai": "^6.32.0",
"otplib": "^12.0.1",
Expand Down
26 changes: 13 additions & 13 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
},
"private": true,
"dependencies": {
"@angular/animations": "~20.3.16",
"@angular/animations": "~20.3.18",
"@angular/cdk": "~20.2.14",
"@angular/common": "~20.3.16",
"@angular/compiler": "~20.3.16",
"@angular/core": "~20.3.16",
"@angular/forms": "~20.3.16",
"@angular/common": "~20.3.18",
"@angular/compiler": "~20.3.18",
"@angular/core": "~20.3.18",
"@angular/forms": "~20.3.18",
"@angular/material": "~20.2.14",
"@angular/platform-browser": "~20.3.16",
"@angular/platform-browser-dynamic": "~20.3.16",
"@angular/router": "~20.3.16",
"@angular/platform-browser": "~20.3.18",
"@angular/platform-browser-dynamic": "~20.3.18",
"@angular/router": "~20.3.18",
"@brumeilde/ngx-theme": "^1.2.1",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/noto-sans": "^5.2.10",
Expand Down Expand Up @@ -54,7 +54,7 @@
"knip": "^5.79.0",
"libphonenumber-js": "^1.12.9",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lodash-es": "^4.17.23",
"mermaid": "^11.12.1",
"monaco-editor": "0.55.1",
"ng-dynamic-component": "^10.8.0",
Expand All @@ -65,7 +65,6 @@
"pluralize": "^8.0.0",
"postgres-interval": "^4.0.2",
"posthog-js": "^1.341.0",
"private-ip": "^3.0.2",
"puppeteer": "^24.29.1",
"rxjs": "^7.4.0",
"tslib": "^2.8.1",
Expand All @@ -77,8 +76,8 @@
"@angular-devkit/build-angular": "20",
"@angular/build": "20.3.14",
"@angular/cli": "~20.3.14",
"@angular/compiler-cli": "~20.3.16",
"@angular/language-service": "~20.3.16",
"@angular/compiler-cli": "~20.3.18",
"@angular/language-service": "~20.3.18",
"@sentry-internal/rrweb": "^2.16.0",
"@storybook/angular": "^10.2.14",
"@types/node": "^22.10.2",
Expand All @@ -92,7 +91,8 @@
},
"resolutions": {
"mermaid": "^11.10.0",
"webpack": "5.104.1"
"webpack": "5.104.1",
"lodash-es": "4.17.23"
},
"packageManager": "yarn@1.22.22"
}
14 changes: 12 additions & 2 deletions frontend/src/app/validators/hostname.validator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import is_ip_private from 'private-ip';
import * as ipaddr from 'ipaddr.js';
import isFQDN from 'validator/es/lib/isFQDN';
import isIP from 'validator/es/lib/isIP';
import { DBtype } from '../models/connection';

const PRIVATE_RANGES = new Set(['private', 'loopback', 'linkLocal', 'unspecified', 'carrierGradeNat', 'uniqueLocal']);

function isPrivateIP(ip: string): boolean {
try {
return PRIVATE_RANGES.has(ipaddr.process(ip).range());
} catch {
return false;
}
}
Comment on lines +7 to +15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify ipaddr.js range values and behavior with IPv6-mapped addresses
node -e "
const ipaddr = require('ipaddr.js');
const testIPs = [
  '192.168.1.1',
  '10.0.0.1',
  '127.0.0.1',
  '::ffff:192.168.1.1',
  '::ffff:10.0.0.1',
  '::1'
];
testIPs.forEach(ip => {
  try {
    const parsed = ipaddr.process(ip);
    console.log(ip + ' -> range: ' + parsed.range());
  } catch (e) {
    console.log(ip + ' -> error: ' + e.message);
  }
});
"

Repository: rocket-admin/rocketadmin

Length of output: 904


🏁 Script executed:

# Find the hostname validator file and check if it exists
git ls-files | grep -E "hostname\.validator\.(ts|js)"

Repository: rocket-admin/rocketadmin

Length of output: 117


🏁 Script executed:

# Check if ipaddr.js is in the dependencies
find . -name "package.json" -type f | head -5 | xargs grep -l "ipaddr" 2>/dev/null || echo "No package.json found with ipaddr"

Repository: rocket-admin/rocketadmin

Length of output: 91


🏁 Script executed:

# Check the actual validator file to see the context
cat -n frontend/src/app/validators/hostname.validator.ts 2>/dev/null || echo "File not found at expected path"

Repository: rocket-admin/rocketadmin

Length of output: 1545


🌐 Web query:

ipaddr.js range() method IPv6-mapped addresses behavior documentation

💡 Result:

The ipaddr.js library provides a range method on both IPv4 and IPv6 address instances. This method returns a predefined name (e.g., "unicast", "reserved", "private") for special IP ranges defined by protocols, based on matching the address against hardcoded CIDR ranges in the source code. For IPv6 addresses, including IPv4-mapped addresses (e.g., ::ffff:192.168.1.1), the range method uses IPv6-specific special ranges listed in the source. One explicit range is "ipv4Mapped": [[new IPv6([0, 0, 0, 0, 0, 0xffff, 0, 0]), 96]], covering ::ffff:0:0/96 per RFC 4291. An IPv4-mapped IPv6 address like ipaddr.parse('::ffff:192.168.1.1').range will match the "ipv4Mapped" range if it fits ::ffff:0:0/96, returning "ipv4Mapped". It does not automatically convert to the underlying IPv4 address and use IPv4 ranges (e.g., "private" for 192.168.1.1); that behavior is only in the separate global ipaddr.process method for parsing. The exact ranges are defined in the library source at https://github.com/whitequark/ipaddr.js/blob/main/lib/ipaddr.js (IPv6 around line 530, IPv4 around line 182 as noted in README). Full list for IPv6 includes: unspecified, linkLocal, multicast, loopback, uniqueLocal, ipv4Mapped, rfc6145, rfc6052, 6to4, teredo, as112v6, orchid2, droneRemoteIdProtocolEntityTags, reserved, benchmarking, amt, discard, unicast (default). No official documentation explicitly details IPv4-mapped behavior for range, but source code confirms it treats them as IPv6 addresses matching IPv6 special ranges, specifically identifying them via the "ipv4Mapped" range.

Citations:


Add ipv4Mapped to PRIVATE_RANGES to prevent IPv6-wrapped private IP bypass.

IPv4-mapped IPv6 addresses (e.g., ::ffff:192.168.1.1) are recognized by ipaddr.js with the range ipv4Mapped, which is not in PRIVATE_RANGES. This allows private IP addresses wrapped in IPv6 format to bypass the private IP restriction on line 34.

Fix to include ipv4Mapped range
-const PRIVATE_RANGES = new Set(['private', 'loopback', 'linkLocal', 'unspecified', 'carrierGradeNat', 'uniqueLocal']);
+const PRIVATE_RANGES = new Set(['private', 'loopback', 'linkLocal', 'unspecified', 'carrierGradeNat', 'uniqueLocal', 'ipv4Mapped']);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/validators/hostname.validator.ts` around lines 7 - 15, The
PRIVATE_RANGES set used by isPrivateIP does not include the ipaddr.js range name
for IPv4-mapped IPv6 addresses, allowing ::ffff:192.168.x.x to bypass the check;
update the PRIVATE_RANGES Set (symbol: PRIVATE_RANGES) to include the string
'ipv4Mapped' so that isPrivateIP (function: isPrivateIP) treats IPv4-mapped
addresses as private when ipaddr.process(ip).range() returns 'ipv4Mapped'.


export function hostnameValidation(dbType: DBtype): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (control.value) {
Expand All @@ -21,7 +31,7 @@ export function hostnameValidation(dbType: DBtype): ValidatorFn {
hostname = hostname.replace(/^mongodb\+srv:\/\//, '');
}

if (control.value === 'localhost' || (isIP(control.value) && is_ip_private(control.value)))
if (control.value === 'localhost' || (isIP(control.value) && isPrivateIP(control.value)))
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The localhost/private-IP guard is checking control.value, but you normalize into hostname (stripping https:// for DynamoDB and mongodb+srv:// for MongoDB). This allows bypasses like https://127.0.0.1 / mongodb+srv://127.0.0.1 to pass validation because isIP(control.value) is false while isIP(hostname) is true. Use the normalized hostname for the localhost / private-IP checks (and consider normalizing case for localhost).

Suggested change
if (control.value === 'localhost' || (isIP(control.value) && isPrivateIP(control.value)))
const normalizedHostname = hostname.toLowerCase();
if (normalizedHostname === 'localhost' || (isIP(hostname) && isPrivateIP(hostname)))

Copilot uses AI. Check for mistakes.
return { isLocalhost: true };
if (!(isIP(hostname) || isFQDN(hostname))) return { isInvalidHostname: true };
Comment on lines +34 to 36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Security: Private IP check bypassed for DynamoDB connections.

The private IP validation at line 34 uses control.value (raw input), but for DynamoDB the hostname is extracted from the https:// prefixed URL. When a user enters https://192.168.1.1:

  1. isIP("https://192.168.1.1") returns false, skipping the private IP check
  2. hostname = "192.168.1.1" passes the isIP(hostname) check at line 36

This defeats the SSRF mitigation this PR intends to address. The check should use the processed hostname variable.

🔒 Proposed fix to check processed hostname
-			if (control.value === 'localhost' || (isIP(control.value) && isPrivateIP(control.value)))
+			if (hostname === 'localhost' || (isIP(hostname) && isPrivateIP(hostname)))
 				return { isLocalhost: true };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/app/validators/hostname.validator.ts` around lines 34 - 36, The
private IP/localhost check currently uses control.value which can include a
scheme and bypass the SSRF check; update the conditional to use the processed
hostname variable instead (e.g., replace uses of control.value in the first if
with hostname) so that you call isIP(hostname) && isPrivateIP(hostname) and
compare hostname === 'localhost' before validating isIP(hostname) ||
isFQDN(hostname); keep the existing calls to isIP, isPrivateIP and isFQDN and
ensure the check occurs prior to the final validity check.

}
Expand Down
Loading
Loading