{"items":[{"id":"NVI-00015341","kind":"vulnerability","title":"dssrf has an SSRF bypass with remove_at_symbol_in_string","revision":1,"unknowns":["Customer inventory, deployment, reachability and exploitation have not been inspected."],"lifecycle":"active","identifiers":["GHSA-cg4g-m8jx-vjv2","CVE-2026-54722"],"plain_summary":"GitHub lists this reviewed advisory for dssrf. Published version ranges and source prerequisites are shown below; this is not an inspection of a visitor’s application.","review_status":"source_listed","control_topics":[],"public_actions":[{"summary":"Identify the exact component and deployed version, examine the linked publisher’s conditions, and retain evidence of any mitigation.","source_ids":["sr-5d320205ad8cfdde2c505939fdf4d2c4"],"action_type":"investigate","limitations":"Confirm current vendor support, compatibility and deployment evidence. This report has not tested a fix in your system.","current_supported_target":null}],"schema_version":"1.1.0","source_metrics":[],"why_it_matters":"Published affected context helps an owner decide what evidence to gather. This report has not examined your application.","related_sources":[{"id":"sr-5d320205ad8cfdde2c505939fdf4d2c4","url":"https://github.com/advisories/GHSA-cg4g-m8jx-vjv2","basis":"vendor_advisory","notes":"GitHub Advisory Database; CC-BY-4.0. Each advisory links its original attribution. Null normalizes fields.","title":"GHSA-cg4g-m8jx-vjv2","publisher":"GitHub Advisory Database","observed_at":"2026-11-03T09:58:49.818Z"}],"affected_context":[{"certainty":"source_listed","source_ids":["sr-5d320205ad8cfdde2c505939fdf4d2c4"],"technology":"dssrf","known_exclusions":[],"conditions_summary":"## Summary\n\n`is_url_safe` in v1.0.3 contains an SSRF bypass. `remove_at_symbol_in_string` is applied to the raw URL string **before** `new URL()` parses it. This strips the `@` that separates userinfo from host, corrupting the hostname so internal IPs are never checked.\n\n## Vulnerability\n\nIn `helpers.ts`, `is_url_safe` does:\n\n```ts\nu = remove_at_symbol_in_string(u);   // strips ALL '@' from the raw string\n// ...\nconst parsed = new URL(u);\nconst hostname = parsed.hostname;    // resolved from the corrupted string\n```\n\n### What happens step by step\n\nInput: `http://evil.com@127.0.0.1/`\n\n1. `remove_at_symbol_in_string` → `http://evil.com127.0.0.1/`\n2. `new URL(...)` → `hostname = \"evil.com127.0.0.1\"`\n3. Not a bare IP, not IPv6 → passes all IP checks\n4. `is_hostname_resolve_to_internal_ip(\"evil.com127.0.0.1\")` → NXDOMAIN → returns false\n5. **Result: `true` (safe)** — but any HTTP client using the *original* URL connects to `127.0.0.1`\n\n### Proof of Concept\n\n```js\nimport nock from 'nock';\nimport { got } from 'got';\nimport { is_url_safe } from 'dssrf';\n\n// Simulate an internal server at 10.0.0.1 that returns secret data\nnock('http://10.0.0.1:80').persist().get('/').reply(200, 'SECRET_DATA","public_affected_ranges":["<= 1.0.3"]}],"null_published_at":"2026-11-03T10:03:26.571Z","source_updated_at":"2026-07-30T21:23:16.000Z","source_disclosed_at":"2026-07-30T16:26:52.000Z","is_synthetic_fixture":false,"null_last_reviewed_at":null,"public_change_summary":"First indexed by Null; source disclosure date is shown separately.","null_first_observed_at":"2026-11-03T09:58:49.818Z"}],"next_cursor":null,"snapshot_revision":"42594"}