Skip to content

security(cf-container-hello): unauthenticated probe Worker fronts a stale unhardened crawler copy — SSRF / file:// fetch (from PR #935) #955

Closed

Description

@dezren39

Finding — security: pkgs/cf-container-hello ships an unauthenticated Worker in front of a stale, unhardened crawler copy (SSRF / local-file fetch)

PR #935 (merged, head 92d0c6ee, commits 4a508974, 60656e73, 92d0c6ee) added the diagnostic probe package pkgs/cf-container-hello and kept it as the "known-good container reference". Two halves combine into an exposed fetch proxy:

  1. The Worker has no auth gate at all. pkgs/cf-container-hello/src/index.ts (whole file, verified on main a2eef6f7) contains no Authorization / token check. fetch() routes by path prefix: /probe… forwards the request as-is to the CrawlerProbe container (getContainer(env.CRAWLER_PROBE, "probe-2").fetch(request)), /lite… and /name/… forward to other containers, and anything else goes to the hello container.
  2. The container it fronts is the pre-hardening crawler. pkgs/cf-container-hello/image/app.py Handler.do_POST (~lines 76–103) does url = json.loads(self.rfile.read(length))["url"] then urllib.request.urlopen(Request(url), timeout=20, context=CTX) — no scheme allowlist, no localhost/private-IP policy, no redirect re-validation, and returns the fetched page's extracted text (up to 20,000 chars) as JSON.

Contrast with the production crawler this was copied from: pkgs/lab-ops-hub/container/app.py carries an extensive validate_crawl_url SSRF policy (the #441 policy, hardened further in #516/#526/#529/#572/#593/#614 — public http(s) only, no localhost-style names, no non-global IP literals, redirect targets held to the same policy, container re-validates independently as defence-in-depth), and pkgs/lab-ops-hub/src/index.ts gates /api/* behind the API_TOKEN secret, fail-closed (503 when unconfigured, 401 when unauthenticated). The probe copy has none of either layer. The deployed image for the probe is also a stale fork: pkgs/cf-container-hello/image/container-image-v4.nix builds this 103-line app.py, while production moved to :v5 built from the hardened container/app.py (PR #935 body).

Verified locally (2026-10-26, this run)

Ran the exact committed pkgs/cf-container-hello/image/app.py on localhost and probed POST /crawl:

  • {"url":"file:///etc/hostname"} → handler returned 200. urllib accepted the file:// scheme and read the local file — there is no scheme validation anywhere in the handler. (Extracted text was empty for that file in the sandbox, but the fetch executed; on the container image the same path reads whatever the process can read and returns its text content.)
  • {"url":"http://127.0.0.1:8080/"} → the loopback fetch was attempted with no policy block (it blocked the single-threaded server on itself until the client timed out). No localhost/private-range check exists.

Why it matters

PR #935's body says the clean-room Worker was deployed and used live for the container diagnosis, and the package is kept (README: "known-good container reference", wrangler.jsonc name cf-container-hello). If that deployment (workers.dev or any route) is still live, anyone on the internet has an unauthenticated SSRF/local-file fetch proxy that also burns container minutes. Even if it is currently undeployed, the committed source deploys that way by default — the next diagnostic session that runs npm run deploy re-exposes it.

This is a single-repo finding (code only): the production lab-ops-hub crawler is hardened and gated; only the probe copy is exposed.

Acceptance criteria

  • Determine whether the cf-container-hello Worker is currently deployed/routed anywhere; record the answer on this issue. If live and unauthenticated, take it down or gate it immediately.
  • pkgs/cf-container-hello/src/index.ts: every route that forwards to a container (at minimum /probe) requires the same class of bearer-token gate lab-ops-hub uses, fail-closed when the secret is unset — or the forwarding routes are removed.
  • pkgs/cf-container-hello/image/app.py: the stale crawler copy is eliminated as a divergence risk — either build the probe image from the production hardened pkgs/lab-ops-hub/container/app.py, or port its validate_crawl_url policy verbatim (public http(s) only; no file:/other schemes; no localhost/private/non-global targets; redirects re-validated).
  • A regression test/probe shows file://, http://127.0.0.1/, a private-range IP literal, and a redirect to a private target are all rejected by the probe crawler, and an unauthenticated POST /probe/crawl is rejected by the Worker.

Activity

  1. added
    kind/securitySecurity issue or security hardening
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/highImportant and time-sensitive
    queue/queuedHas a queued task in the Supabase agent queue
    app/lab-ops-hubPackage/app: lab-ops-hub (pkgs/ or apps/ in this repo)
    app/cf-container-helloPackage/app: cf-container-hello (pkgs/ or apps/ in this repo)
    on Oct 26, 2026
  2. dezren39 commented on Oct 26, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: filed

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 26, 2026
  4. 10 remaining items

  5. dezren39 commented on Oct 26, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: queue worker (repo-worker-code)
    • Session / ID: queue task 1104 (resumed by repo-queue-worker-shift cron run, 2026-10-26 14:45Z)
    • Found via: queue claim
    • Task: Resume prior launch's in-progress fix: verify the worktree diff against the acceptance criteria, commit, push, open PR.
    • Expectation: m — done = PR open with the probe Worker gated/removed and the stale crawler copy eliminated, or task completed/parked with evidence.
    • Links: branch queue/1104-955-cf-container-hello-auth, worktree ~/git/wt-1104
  6. added a commit that references this issue on Oct 26, 2026
  7. dezren39 commented on Oct 26, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up → gate deployed live

    Deployment status (acceptance #1): LIVE, was unauthenticated — now gated.

    • Cloudflare API: script cf-container-hello deployed (latest pre-fix deployment 2026-10-26T01:01Z), no custom domains, served at cf-container-hello.developing-today.workers.dev. Pre-fix unauthenticated probes: GET /probe/ → 200 from the crawler container; GET /worker-marker → 200.
    • Disabling the workers.dev subdomain via API was refused for both stored Cloudflare tokens (10405 Method not allowed for this authentication scheme), so the exposure was closed by deploying the gated Worker from this PR's branch (version 3a04cb82-1be0-4f75-a097-771f599f176e) and setting the API_TOKEN secret (generated this run, filed in Bitwarden Secrets Manager as cloudflare.cf-container-hello-api-token, mirroring the lab-ops-hub pattern).
    • Post-deploy live verification: GET / unauth → 401; GET /probe/ unauth → 401; POST /probe/crawl unauth → 401; GET /probe/ with bearer → 200 from the hardened v5 crawler container; GET /worker-marker → 200 (public by design, starts no container).
    • Note: through this Worker the container still receives the /probe-prefixed path, so its POST /crawl route 404s end-to-end (pre-existing path behaviour, unchanged by this fix); the SSRF policy itself is pinned by the regression tests against the production validator the probe image now runs.

    Remaining: PR #978 CI re-check after the treefmt fix, then merge under the standing criteria.

  8. added a commit that references this issue on Oct 26, 2026
  9. dezren39 commented on Oct 26, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    • Agent: queue worker (repo-worker-code)
    • Session / ID: queue task 1104 (repo-queue-worker-shift cron run, 2026-10-26)
    • Found via: queue claim
    • Task: Triage/fix complete. PR fix(cf-container-hello): fail-closed API_TOKEN gate, drop stale crawler copy (#955) #978 merged (merge commit 24261355); all checks green (treefmt, run, Nix flake evaluation) after a ruff-format fix to the new test.
    • Outcome: Worker container routes fail-closed behind API_TOKEN (deployed live and verified: unauth 401 on /, /probe/, POST /probe/crawl; authed 200 from the hardened v5 crawler; token filed in Bitwarden as cloudflare.cf-container-hello-api-token); stale crawler copy deleted — CrawlerProbe runs the production lab-ops-hub-crawlercontainer:v5; 11 regression tests pass. Live exposure closed by the deploy, not just by the merge.
    • Links: PR fix(cf-container-hello): fail-closed API_TOKEN gate, drop stale crawler copy (#955) #978 · branch queue/1104-955-cf-container-hello-auth
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/cf-container-helloPackage/app: cf-container-hello (pkgs/ or apps/ in this repo)app/lab-ops-hubPackage/app: lab-ops-hub (pkgs/ or apps/ in this repo)kind/securitySecurity issue or security hardeningpriority/highImportant and time-sensitivesource/pr-diff-reviewFiled by the pr-diff-review jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions