highCVE-2026-54722Jul 30, 2026

CVE-2026-54722: dssrf SSRF Bypass via Userinfo Stripping

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

The dssrf library's URL safety check can be tricked into approving requests to internal IP addresses by placing credentials before the host in the URL, causing the validator to inspect the wrong…

Packagedssrf
Ecosystemnpm
Affected<= 1.0.3
Fixed in1.0.4
CVE-2026-54722: dssrf SSRF Bypass via Userinfo Stripping

The problem

dssrf versions up to and including 1.0.3 call remove_at_symbol_in_string on the raw URL string before passing it to new URL(). The @ character in a URL separates userinfo (credentials) from the actual host. Stripping it first corrupts the string, so the parser resolves a mangled hostname instead of the real target.

An attacker supplies a URL like http://2@10.0.0.1/. After @ is stripped, the parser sees http://210.0.0.1/, which passes all internal-IP checks. The HTTP client, however, uses the original URL and connects to 10.0.0.1. Any internal IPv4 range, IPv6 loopback, and the AWS IMDS address are reachable this way.

Proof of concept

A working proof-of-concept for CVE-2026-54722 in dssrf, with the exact payload below.

javascript
// is_url_safe returns true (safe) for all of these in dssrf <= 1.0.3

const BYPASS_1 = 'http://2@10.0.0.1/';            // RFC 1918
const BYPASS_2 = 'http://x@127.0.0.1/';            // loopback
const BYPASS_3 = 'http://x@169.254.169.254/';       // AWS IMDS
const BYPASS_4 = 'http://x@192.168.1.1/';           // RFC 1918

// Validator is fooled; HTTP client still reaches the internal host:
import { is_url_safe } from 'dssrf';
import { got } from 'got';

console.log(await is_url_safe(BYPASS_1));  // true  <- WRONG
const res = await got(BYPASS_1, { retry: { limit: 0 } });
console.log(res.body);                     // response from 10.0.0.1

The root cause is operation-order confusion: remove_at_symbol_in_string mutates the raw URL string before the URL parser has a chance to interpret authority syntax. In RFC 3986, the @ delimiter marks the boundary between userinfo and host, so removing it first merges those two components into a single, unrecognised hostname.

The validator then checks that invented hostname rather than the real one.

The patch (commit 9211f91, PR #98) removes the pre-parse strip entirely and instead checks parsed.username and parsed.password after new URL() has run. If either field is non-empty, the function returns false immediately. This correctly rejects userinfo without corrupting the hostname the IP checks operate on.

CWE-791 (Improper Neutralization of Equivalent Special Elements) applies: @ in a raw string and @ as an authority delimiter are semantically different, and the code treated them as equivalent before normalization.

The fix

Upgrade to dssrf 1.0.4. The fix removes the remove_at_symbol_in_string pre-processing step and adds an explicit userinfo rejection after parsing: any URL whose parsed.username or parsed.password is non-empty is now returned as unsafe. No API changes are required on the caller side.

Reporter not attributed.

References: [1][2][3][4][5]

Related research