Jul 30, 2026·6 min read·71 visits
The dssrf-js library before 1.0.4 strips the '@' character from URLs during safety validation. This causes the library to validate a safe, mutated host string (e.g., evil.com127.0.0.1) while the downstream client retrieves the original URL, connecting directly to restricted local resources.
An SSRF validation bypass exists in dssrf-js (v1.0.3 and prior) due to an improper string normalization sequence inside is_url_safe. Before validating the host using Node's WHATWG parser, the helper strips the '@' symbol. This corrupts the parser's authority resolution, while the application's client requests the original, un-sanitized string containing internal IP targets.
The dssrf-js library is a Node.js utility designed to protect applications from Server-Side Request Forgery (SSRF) attacks. It intercepts user-supplied destination URLs, sanitizing and validating them to prevent connections to loopback networks, private IP spaces, and local metadata endpoints. This is typically achieved by parsing the host, resolving the domain, and cross-referencing IPs against blocklists.
In versions prior to 1.0.4, the library performs unsafe pre-processing on the raw URL string before handing it over to the parsing engine. This design flaw introduces a parser differential between the sanitizer's internal validation context and the downstream client's execution context. As a result, input designed to exploit this differential bypasses safety validation altogether.
Because the validator asserts that the input is safe, the application proceeds to execute the request using the original, unmodified URL. This allows remote attackers to target internal systems, accessing local administrative dashboards, AWS metadata services, and databases without authorization.
The root cause of this vulnerability lies in the helper function remove_at_symbol_in_string executed within is_url_safe. In version 1.0.3, this function strips all occurrences of the '@' character from the raw URL string before invoking the WHATWG parser.
According to the WHATWG URL standard, the '@' character separates optional userinfo credentials from the host segment in a URL authority blocks: scheme://username:password@hostname/path. When a client parses a URL with this format, it routes the network connection exclusively to the designated host, treating the preceding authority data as credentials.
When an attacker provides an authority-based URL like http://evil.com@127.0.0.1/, the true network destination is the local IP address 127.0.0.1. However, the validator's stripping routine modifies this input string to http://evil.com127.0.0.1/ before validation. The WHATWG parser processes the modified string and extracts the hostname as evil.com127.0.0.1. Because this hostname points to a public namespace or fails DNS lookup completely, the library mistakenly marks the URL as safe. The application then executes the HTTP request against the original, un-mutated URL, making a direct connection to the internal loopback.
The workflow diagram below demonstrates how the validation bypass operates step-by-step, contrasting the path taken by the validator with the path taken by the client application.
This execution discrepancy illustrates why string manipulation must never be executed on raw URLs prior to parsing and structure verification.
Comparing the pre-patched codebase with the updated implementation highlights the structural changes introduced to address this parser differential.
// VULNERABLE: dist/helpers.js (v1.0.3 and prior)
async function is_url_safe(url) {
try {
let u = normalize_unicode(url);
u = replace_backslash_with_slash_in_string(u);
u = replace_two_slashes_url_to_normal_url(u);
u = remove_at_symbol_in_string(u); // Destructive modification of raw URL
const schema = normalize_schema(u);
if (!is_proto_safe(schema)) return false;
const parsed = new URL(u);
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
if (await is_hostname_resolve_to_internal_ip(hostname)) return false;
return true;
} catch {
return false;
}
}In the patched release (v1.0.4), the destructive raw string modification of the '@' symbol has been completely removed. Instead, the validation library delegates structure parsing directly to a new is_parsed_url_safe function.
// PATCHED: dist/helpers.js (v1.0.4)
async function is_parsed_url_safe(parsed) {
if (!is_proto_safe(parsed.protocol)) return false;
// Explicitly reject URLs containing userinfo structures
if (parsed.username !== "" || parsed.password !== "") return false;
const hostname = parsed.hostname.replace(/^\[|\]$/g, "");
if (await is_hostname_resolve_to_internal_ip(hostname)) return false;
return true;
}
async function is_url_safe(url) {
try {
let u = normalize_unicode(url);
u = replace_backslash_with_slash_in_string(u);
u = replace_two_slashes_url_to_normal_url(u);
const parsed = new URL(u); // Parsed directly on minimally normalized input
if (!await is_parsed_url_safe(parsed)) return false;
return true;
} catch {
return false;
}
}This fix successfully addresses the core design flaw by evaluating the actual structure parsed by Node's URL library and ensuring that credentials blocks are completely rejected, rendering the attack vector non-viable.
To exploit this vulnerability, the target application must rely on dssrf-js version < 1.0.4 and execute requests with the original, unmodified URL string. An attacker must also identify an endpoint that passes input to the security helper before dispatching the payload to the internal infrastructure.
Consider a target application exposing a preview routing service at https://target.app/preview?url=.... An attacker executes the following command to target an internal Redis server running on the loopback adapter:
curl -v "https://target.app/preview?url=http://example.com@127.0.0.1:6379/"The backend server calls is_url_safe('http://example.com@127.0.0.1:6379/'). The library strips the '@' character, yielding http://example.com127.0.0.1:6379/. Since the hostname example.com127.0.0.1 resolves to a public DNS destination or fails, validation returns success. The library then passes the original URL to the HTTP client (e.g., got, axios), which connects directly to 127.0.0.1 on port 6379, completely bypassing the SSRF defense.
The impact of this SSRF bypass is classified as High. An attacker with network access to the vulnerable endpoint can manipulate downstream requests to interact with restricted infrastructure. This includes reaching cloud metadata services (such as AWS IMSv1 at 169.254.169.254), administrative systems, databases, and local daemon sockets.
While the patch successfully blocks userinfo-based bypass attempts, security engineers must recognize that third-party validation wrappers are always prone to residual architectural risks. First, the Time-of-Check to Time-of-Use (TOCTOU) bug class remains. If dssrf-js validates a domain pointing to a safe external IP, a subsequent DNS change can redirect the client's request to an internal network target during connection establishment.
Second, discrepancies in underlying parsing engines remain a concern. If the backend HTTP client interprets complex URL components differently than the Node.js WHATWG parser, attackers may find similar routing anomalies. Applications should rely on network-level segregation and request pinning instead of software-only URL validation wrappers.
The definitive solution is to upgrade dssrf-js to version 1.0.4 or later. This removes the vulnerable string processing helper and enforces strict userinfo validation checks inside the core module.
For systems that cannot be immediately updated, security teams can implement an application-level input validation filter. Any input string containing the '@' character in its authority block should be dropped immediately before invoking dssrf-js logic.
Additionally, host firewalls, cloud security groups, and local Kubernetes NetworkPolicies should be configured to drop egress traffic originating from application processes directed at private subnets or internal metadata targets. This ensures that even if validation is bypassed, network-level routing policies block unauthorized connections.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
dssrf-js HackingRepo | < 1.0.4 | 1.0.4 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-76: Improper Neutralization of Equivalent Special Elements |
| Attack Vector | Network (AV:N) |
| CVSS v4.0 Score | 8.7 |
| EPSS Score | N/A (New 2026 Vulnerability) |
| Impact | High Integrity Compromise (VI:H) |
| Exploit Status | None (No active public exploits cataloged) |
| KEV Status | False |
The software receives an input that can be represented in multiple equivalent ways, but fails to neutralize all equivalent representations before processing the input.
Contao CMS versions 4.1.0 through 5.3.49 and 5.4.0-RC1 through 5.7.11 fail to validate form submission tokens and enforce rate limiting when processing activation email resend requests via HTTP POST, enabling resource exhaustion and account state enumeration.
An information disclosure vulnerability in Contao CMS allows unauthenticated site visitors to view protected page titles, URLs, and text excerpts through search queries when protected page indexing is disabled after previously being enabled.
In Vikunja prior to version 2.6.0, relation creation via the CalDAV endpoint fails to invoke the TaskRelation.CanCreate authorization check. This missing access control allows an authenticated user to establish unauthorized relationships and perform write operations against any task, provided its unique identifier (UID) is known.
A cross-project information disclosure vulnerability in Vikunja allows authenticated users with read access to one project to view private task details from unauthorized projects via subtask expansion parameters.
An information disclosure vulnerability in Vikunja allows authenticated users with read access to a task to expose private email addresses of assigned users through the API task assignees endpoint due to an unmasked database query.
An authorization bypass vulnerability in Vikunja versions prior to v2.6.0 permits authenticated users to delete relationships between tasks across project boundaries without requiring read or write authorization for the target related task.