Fix yarn audit vulnerabilities across backend and frontend#1692
Conversation
- Upgrade nodemailer ^8.0.2 → ^8.0.4 (SMTP command injection, low) - Upgrade @angular/* ~20.3.16 → ~20.3.18 (XSS in i18n bindings, high) - Upgrade lodash-es ^4.17.21 → ^4.17.23 + resolution (prototype pollution, moderate) - Replace private-ip with ipaddr.js for private IP detection (SSRF, high, no patch available) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThree dependency versions updated across backend and frontend packages: nodemailer from ^8.0.2 to ^8.0.4, Angular runtime and dev dependencies from ~20.3.16 to ~20.3.18, and lodash-es from ^4.17.21 to ^4.17.23. The private-ip dependency was removed and replaced with ipaddr.js in the hostname validator logic. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
This PR updates backend and frontend Node dependencies to address yarn audit security vulnerabilities, including replacing the unpatched private-ip package with ipaddr.js for private IP detection in the frontend.
Changes:
- Upgraded
nodemailerin the backend from^8.0.2to^8.0.4. - Upgraded
@angular/*packages in the frontend from~20.3.16to~20.3.18. - Upgraded/pinned
lodash-esto4.17.23(including a Yarn resolution) and removedprivate-ipfrom the frontend dependency graph.
Reviewed changes
Copilot reviewed 3 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| yarn.lock | Updates backend workspace lock entries for nodemailer@^8.0.4. |
| frontend/yarn.lock | Updates Angular packages, removes private-ip and related deps, and updates lodash-es lock entries. |
| frontend/src/app/validators/hostname.validator.ts | Replaces private-ip usage with ipaddr.js-based private IP detection. |
| frontend/package.json | Bumps Angular versions, updates lodash-es, removes private-ip, and adds a lodash-es resolution. |
| backend/package.json | Bumps nodemailer version. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| if (control.value === 'localhost' || (isIP(control.value) && is_ip_private(control.value))) | ||
| if (control.value === 'localhost' || (isIP(control.value) && isPrivateIP(control.value))) |
There was a problem hiding this comment.
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).
| if (control.value === 'localhost' || (isIP(control.value) && isPrivateIP(control.value))) | |
| const normalizedHostname = hostname.toLowerCase(); | |
| if (normalizedHostname === 'localhost' || (isIP(hostname) && isPrivateIP(hostname))) |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/app/validators/hostname.validator.ts`:
- Around line 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.
- Around line 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'.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 201c4ac1-7058-4dcb-9380-e4dd096f4c1a
⛔ Files ignored due to path filters (2)
frontend/yarn.lockis excluded by!**/yarn.lock,!**/*.lockyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (3)
backend/package.jsonfrontend/package.jsonfrontend/src/app/validators/hostname.validator.ts
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://github.com/whitequark/ipaddr.js/blob/main/README.md
- 2: https://www.npmjs.com/package/ipaddr.js
- 3: https://github.com/whitequark/ipaddr.js/
- 4: https://github.com/whitequark/ipaddr.js/blob/main/lib/ipaddr.js
- 5: https://github.com/whitequark/ipaddr.js/blob/main/lib/ipaddr.js.d.ts
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'.
| if (control.value === 'localhost' || (isIP(control.value) && isPrivateIP(control.value))) | ||
| return { isLocalhost: true }; | ||
| if (!(isIP(hostname) || isFQDN(hostname))) return { isInvalidHostname: true }; |
There was a problem hiding this comment.
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:
isIP("https://192.168.1.1")returnsfalse, skipping the private IP checkhostname = "192.168.1.1"passes theisIP(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.
Summary by CodeRabbit