feat(ssrf-guard-js): Workers demo — the first Node demo in this repo - #110
Merged
Conversation
Closes the roadmap's "a JS demo in devslab-examples": all 20 demos here
were JVM, so the JS/TS sibling had nothing showing it, and the Workers
half of the library — the half built for edge runtimes — had no worked
example at all.
The demo is the edge story rather than a translation of the Spring ones.
On Workers there is no usable dns.lookup, and even if there were, fetch
resolves the host itself, so a userland DNS check cannot be pinned to the
socket that connects. The library's answer is to refuse rather than
pretend: safeFetch throws a typed error pointing at guardedFetch instead
of degrading into a weaker check that looks like the same call. There is
an endpoint for exactly that.
Four routes, chosen to make one distinction concrete:
/api-call uses the Hono middleware, because the endpoint is known
ahead of time and a fixed allowlist can be enforced BEFORE
the handler runs
/crawl cannot, and that is not an oversight — its policy is derived
from what the user submitted, so a fixed allowlist would
reject every submission and a permissive one would claim a
safety property it does not have
Also /tool-input (scanEmbedded) and /attack-matrix, which answers 17
payloads with checkUrl so one bad entry does not end the report.
Three of my own test expectations were wrong before they were right, and
each was the library behaving correctly:
- a metadata URL over http fails on the SCHEME, not the IP literal,
because the policy pinned https;
- the attack matrix allows ONE row, not two — singleHostPolicy locks
the port, so the same host on the default port is blocked_port. That
row is now the one worth reading in the table;
- /why-no-safe-fetch cannot be asserted on its message, because the
tests run on NODE, where node:dns exists and safeFetch gets as far as
a real lookup. It is asserted on response shape, and both READMEs say
why. Asserting the Workers message there would be a test passing for
the wrong reason.
CI: the detect job found demos by build.gradle.kts alone, so a Node demo
was invisible to it. It now detects package.json demos into a separate
matrix with its own pnpm job, rather than one list the build job would
have to sniff. Validated by parsing the workflow and re-running the
detection locally — it picks up exactly this one directory.
Verified: pnpm verify green (typecheck + 11 tests), no network and no
Cloudflare account needed.
The previous commit added the Node build job, the matrix output and the
filtering loops — but not the one line that finds the directories. The
edit that was supposed to insert it contained a `\n` that a scripted
replacement mangled, the replacement silently matched nothing, and I
validated the result by parsing the YAML, which was perfectly valid and
perfectly wrong.
CI said so plainly: "No Node demos to build" on a PR that adds one.
Fixed, and this time verified by RUNNING the detect script against this
branch rather than reading it:
No demos to build.
Node demos to build:
- ssrf-guard-js-workers-demo
Which is the correct answer for this PR — no Gradle demo changed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
로드맵의 P3-2 "devslab-examples의 JS 데모" 를 닫습니다. 여기 데모 20개가 전부 JVM이라 JS/TS 자매를 보여주는 게 없었고, 특히 엣지 런타임용으로 만든 절반에 예제가 하나도 없었습니다.
Spring 데모의 번역이 아니라 엣지 이야기
Workers에는 쓸 수 있는
dns.lookup이 없고, 있어도fetch가 호스트를 자체 해석하므로 유저랜드 DNS 검사를 실제 연결 소켓에 고정할 수 없습니다.라이브러리의 답은 아닌 척하지 않는 것입니다 —
safeFetch가guardedFetch를 가리키는 타입 있는 에러를 던지지, 같은 호출처럼 보이는 약한 검사로 격하되지 않습니다. 그걸 보여주는 엔드포인트를 따로 뒀습니다.라우트 4개, 한 가지 구분을 위해
/api-call/crawl나머지는
/tool-input(scanEmbedded)과/attack-matrix(페이로드 17종을checkUrl로 — 하나가 나빠도 보고가 끊기지 않게).제 기대값이 세 번 틀렸고, 셋 다 라이브러리가 옳았습니다
http://메타데이터 URL은 IP 리터럴이 아니라 스킴에서 먼저 걸립니다 — 정책이 https를 고정했으니까요.singleHostPolicy가 포트를 잠그므로 같은 호스트라도 기본 포트는blocked_port. 그 줄이 이 표에서 제일 읽어볼 값어치가 있는 줄이 됐습니다./why-no-safe-fetch는 메시지로 단언할 수 없습니다 — 테스트가 Node에서 돌고, 거기엔node:dns가 있어서safeFetch가 실제 조회까지 갑니다. 응답 모양만 단언하고 이유를 양어 README에 적었습니다. 거기서 Workers 메시지를 단언하면 엉뚱한 이유로 통과하는 테스트가 됩니다.CI
detect 잡이
build.gradle.kts로만 데모를 찾아서 Node 데모가 보이지 않았습니다.package.json데모를 별도 매트릭스로 감지하고 전용 pnpm 잡을 붙였습니다 — 빌드 잡이 종류를 냄새 맡게 하는 대신에요.워크플로를 파싱해 검증하고 감지 로직을 로컬에서 재현했습니다. 정확히 이 디렉터리 하나만 잡습니다.
검증
pnpm verify그린 — typecheck + 테스트 11개. 네트워크도 Cloudflare 계정도 불필요합니다(Hono의 request 헬퍼 + fetch 주입).