fix(security): reject userinfo in is_url_safe to prevent SSRF bypass (#97)#98
fix(security): reject userinfo in is_url_safe to prevent SSRF bypass (#97)#98andrewmkhoury wants to merge 3 commits into
Conversation
Stripping '@' from the raw URL string before parsing corrupts the host/userinfo boundary, allowing payloads like http://evil.com@127.0.0.1/ to pass all internal-IP checks. Fix: remove remove_at_symbol_in_string from is_url_safe and instead reject any URL whose parsed .username or .password is non-empty. Also fix tsconfig.json so the project builds cleanly with TS 6. Fixes HackingRepo#97
Review Summary by QodoFix SSRF bypass via userinfo in is_url_safe validation
WalkthroughsDescription• Removes userinfo stripping before URL parsing to prevent SSRF bypass • Adds explicit rejection of URLs with username or password fields • Fixes tsconfig.json for TypeScript 6 compatibility Diagramflowchart LR
A["Raw URL with userinfo<br/>http://evil.com@127.0.0.1/"] --> B["Parse with new URL"]
B --> C["Check parsed.username<br/>and parsed.password"]
C --> D["Reject if non-empty"]
D --> E["SSRF blocked ✓"]
File Changes1. src/helpers.ts
|
Code Review by Qodo
1.
|
Up to standards ✅🟢 Issues
|
- Commit rebuilt dist/ so the shipped entrypoints contain the fix (dist/helpers.js was still carrying the old vulnerable is_url_safe) - Remove remove_at_symbol_in_string from is_redirect_safe (same root cause as is_url_safe — strips '@' before parsing, corrupting host) - Add userinfo rejection (username/password check) to is_redirect_safe for both the initial URL and each redirect target Addresses review comments on HackingRepo#97
All proto, userinfo, and hostname validation is now in one place: is_parsed_url_safe(). Both is_url_safe and is_redirect_safe delegate to it, removing the repeated username/password checks and making redirect-target validation consistent with the primary check. Also removes the now-dead post-parse IPv4 normalization branch — WHATWG URL already normalizes the hostname before we inspect it, so normalize_ipv4 on parsed.hostname was a no-op.
|
we not able to reproduce that @andrewmkhoury relunsec@relunsec:~/software/dssrf$ cat index.js
const { is_url_safe } = require('dssrf');
(async () => {
const cases = [
'http://evil.com@127.0.0.1/',
'http://evil.com@10.0.0.1/',
'http://evil.com@192.168.1.1/',
'http://evil.com@169.254.169.254/', // AWS IMDS
'http://evil.com@[::1]/',
];
for (const url of cases) {
const safe = await is_url_safe(url);
console.log(`${safe ? 'BYPASSED ✗' : 'blocked ✓'} ${url}`);
}
})();
relunsec@relunsec:~/software/dssrf$ node index.js
blocked ✓ http://evil.com@127.0.0.1/
blocked ✓ http://evil.com@10.0.0.1/
blocked ✓ http://evil.com@192.168.1.1/
blocked ✓ http://evil.com@169.254.169.254/
blocked ✓ http://evil.com@[::1]/
relunsec@relunsec:~/software/dssrf$ Everything blocked I installed dssrf via |
|
pong @andrewmkhoury , see my comments |
HackingRepo
left a comment
There was a problem hiding this comment.
see my comments, we not able to reproduce that
|
pong @andrewmkhoury, please address what i said, i will merge you improvements in the next version but not a security vuln, since i see it blocked |
|
There is a confirmed bypass in published 1.0.3. The Verified against the published package: const { is_url_safe } = require('dssrf'); // 1.0.3
await is_url_safe('http://1@10.0.0.1/'); // true ← BYPASSED
await is_url_safe('http://2@10.0.0.1/'); // true ← BYPASSEDYour test used This PR fixes it by checking const { is_url_safe } = require('./dist/utils'); // this PR
await is_url_safe('http://1@10.0.0.1/'); // false ✓
await is_url_safe('http://2@10.0.0.1/'); // false ✓Re: Snyk failure — the job log shows |
|
ok now i able to reproduce it, thank's for that @andrewmkhoury, in another please do'nt post security issues here |
Summary
Fixes #97 — SSRF bypass via userinfo stripping in
is_url_safe(v1.0.3).Root Cause
is_url_safecalledremove_at_symbol_in_stringon the raw URL beforenew URL()parsed it. This stripped the@separator between userinfo and host, corrupting the hostname:Every internal IPv4 range, AWS IMDS (
169.254.169.254), and IPv6 addresses via userinfo all bypassed the guard.Fix
remove_at_symbol_in_stringfromis_url_safeparsed.username !== ""orparsed.password !== ""afternew URL()constructiontsconfig.json(rootDir,types: ["node"],ignoreDeprecations) so the project builds cleanlyVerification
Tested against 15 vectors — all pass:
http://evil.com@127.0.0.1/http://evil.com@10.0.0.1/http://evil.com@192.168.1.1/http://evil.com@169.254.169.254/http://evil.com@[::1]/http://[::1]/(bare IPv6 loopback)http://[fc00::1]/(ULA)http://[fe80::1]/(link-local)http://[::ffff:127.0.0.1]/(IPv4-mapped)http://[::ffff:169.254.169.254]/(IPv4-mapped IMDS)http://[64:ff9b:1::1]/(NAT64 RFC8215)http://[5f00::1]/(SRv6 SID RFC9602)http://[fec0::1]/(site-local)https://www.adobe.com/https://8.8.8.8/