Skip to content

feat(ssrf-guard-js): Workers demo — the first Node demo in this repo - #110

Merged
jlc488 merged 2 commits into
mainfrom
feat/ssrf-guard-js-workers-demo
Aug 9, 2026
Merged

feat(ssrf-guard-js): Workers demo — the first Node demo in this repo#110
jlc488 merged 2 commits into
mainfrom
feat/ssrf-guard-js-workers-demo

Conversation

@jlc488

@jlc488 jlc488 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

로드맵의 P3-2 "devslab-examples의 JS 데모" 를 닫습니다. 여기 데모 20개가 전부 JVM이라 JS/TS 자매를 보여주는 게 없었고, 특히 엣지 런타임용으로 만든 절반에 예제가 하나도 없었습니다.

Spring 데모의 번역이 아니라 엣지 이야기

Workers에는 쓸 수 있는 dns.lookup이 없고, 있어도 fetch가 호스트를 자체 해석하므로 유저랜드 DNS 검사를 실제 연결 소켓에 고정할 수 없습니다.

라이브러리의 답은 아닌 척하지 않는 것입니다 — safeFetchguardedFetch를 가리키는 타입 있는 에러를 던지지, 같은 호출처럼 보이는 약한 검사로 격하되지 않습니다. 그걸 보여주는 엔드포인트를 따로 뒀습니다.

라우트 4개, 한 가지 구분을 위해

/api-call 미들웨어를 씁니다. 엔드포인트가 미리 정해져 있으니 고정 허용 목록을 핸들러 전에 강제
/crawl 쓸 수 없고, 빠뜨린 게 아닙니다. 정책이 사용자 제출값에서 파생됩니다 — 고정 목록이면 모든 제출을 거부하고, 느슨하면 갖지도 않은 안전 속성을 주장

나머지는 /tool-input(scanEmbedded)과 /attack-matrix(페이로드 17종을 checkUrl로 — 하나가 나빠도 보고가 끊기지 않게).

제 기대값이 세 번 틀렸고, 셋 다 라이브러리가 옳았습니다

  1. http:// 메타데이터 URL은 IP 리터럴이 아니라 스킴에서 먼저 걸립니다 — 정책이 https를 고정했으니까요.
  2. 공격 매트릭스에서 허용되는 건 두 줄이 아니라 한 줄입니다. singleHostPolicy가 포트를 잠그므로 같은 호스트라도 기본 포트는 blocked_port. 그 줄이 이 표에서 제일 읽어볼 값어치가 있는 줄이 됐습니다.
  3. /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 주입).

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.
@jlc488
jlc488 merged commit a2789f7 into main Aug 9, 2026
3 checks passed
@jlc488
jlc488 deleted the feat/ssrf-guard-js-workers-demo branch August 9, 2026 08:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant