protect: keep the runtime edge-safe (load Node fs lazily) - #117
Merged
Conversation
The protect runtime is what an EDGE guard imports — the Next edge-middleware template, Cloudflare Workers, Deno, Bun, Supabase Functions. But runtime.js and rules/store.js imported `node:fs`/`node:path` STATICALLY, and the bundler rewrites those to bare `fs`/`path`, which don't resolve on an edge runtime at all: the built dist/protect.js could fail the build or leave the guard unavailable on exactly the platforms we advertise support for. Both filesystem uses are Node-only conveniences and were already fail-open, so load them with a dynamic `await import(…)` instead: - rules/store.js: the disk cache tier (memoized loader; on a filesystem-less runtime it reports "no cache" and the memory / pluggable `ruleCache` tiers carry last-known-good). - runtime.js: the `.patchstackrc.json` api-key fallback (resolveApiKey is now async; createProtection already awaited). Adds tests/protect/edge-safe.test.ts to pin the invariant — no static Node-builtin import anywhere in the runtime graph, and none in the built bundle. The Node disk cache / last-known-good tests still pass unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Lazy-loaded fs imports ensure edge-safe runtime without static Node dependencies. 🎯 Quality: 100% Elite · 📦 Size: Medium 📈 This month: Your 53rd PR — above team average · Averaging Excellent |
The dist assertion could fail spuriously against a STALE bundle left by a build on another branch (CI tests before building, so it skips there and never ran). Gate it on dist being newer than the newest source file; the source-graph assertion is the real invariant and always runs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Author
|
/review |
devlob
approved these changes
Aug 13, 2026
patchstackdave
added a commit
that referenced
this pull request
Aug 13, 2026
* map: trace imported helpers; protect: ship a real edge build (dist/protect.edge.js) Two follow-ups, both driven by tests that prove the behaviour rather than assert a proxy for it. 1. IMPORTED-HELPER TRACING (map). AI-generated apps put data access in a sibling module, so a handler's real sink lives one file away and the endpoint looked sink-free — nothing to correlate a CVE against. The extractor now follows ONE cross-file hop into a relative import (resolving extensionless, /index and the TS-ESM `./db.js` -> db.ts convention), plus one same-file hop inside that module, with a shared parse cache. Bare package specifiers are deliberately NOT followed: a dependency's internals are not this app's attack surface. Coverage notes state the hop limit. 2. A REAL EDGE BUILD. #117 made the Node imports dynamic, which keeps the module loadable off Node — but bundlers FOLLOW dynamic imports, so an edge build still failed to resolve `node:fs`/`node:path`, and the shipped dist/protect.js could NOT be bundled for Next edge middleware / Workers / Deno. (tsup also strips the `node:` prefix, and bare `fs` resolves nowhere on Workers while `node:fs` does under nodejs_compat.) So the source-level "no static node import" check was necessary but not sufficient. Adds scripts/build-edge.mjs: an esbuild build (platform browser) that replaces every Node-only module — and refresh-manifest, which pulls in the lockfile scanner — with a stub that rejects on import. The runtime already treats a failed `import('node:fs')` as "no filesystem here" and falls back to the memory/pluggable rule cache, so behaviour is preserved; the disk cache and manifest re-post are simply unavailable, which is correct on edge. Wired into `npm run build` and selected automatically via package.json exports conditions (workerd / worker / edge-light / deno / browser), with Node still getting the full build. tests/protect/edge-bundle.test.ts is the test that would have caught the original gap: it bundles the shipped artifact the way an edge bundler does (nothing external), asserts zero Node builtin references static OR dynamic, IMPORTS the bundle and verifies it still blocks an exploit with cacheDir set, and pins the exports condition order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * map: recognize Deno / edge platform functions (Supabase Edge Functions, Base44) Platform function runtimes have no router and no route file: one handler per module, invoked by the function's NAME. Without a recognizer, a Supabase Edge Functions or Base44 backend-functions project mapped to NOTHING — no entry points, so no reachability signal and nothing to correlate a vulnerability against. Adds a `Deno.serve(handler)` / `serve(handler)` entry recognizer (entryKind `edge-function`), derives the deployed function name and route from the conventional location (`supabase/functions/<name>/index.ts`, `functions/<name>.ts` → `/<name>`), extends the textual pre-filter so those files are parsed at all, and detects the project shape (`supabase-functions` / `deno-functions`) even when there is no package.json — which is normal for a Deno project. The existing request-read extraction already covers the idiomatic `const { a, b } = await req.json()`, so these endpoints get real inputs, sinks and PROVEN flows: the fixture's `hook -> fetch` (the classic SSRF shape) comes out as a precise flow, which is exactly what a rule needs to pin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * map: prove flows with AST evidence; keep imported helpers in-project and locatable Round-2 review fixes. Both are cases where the previous implementation could make a claim it hadn't earned. 1. FALSE `precise` FLOWS (high). linkFlows concatenated all argument source text and then checked, independently, that (a) the input's leaf name appeared anywhere and (b) any tainted root appeared anywhere. So: const { title } = await req.json(); db.from("items").insert({ title: "system", owner: req.user.id }); was reported `title -> insert [precise]` — `title` matched a property KEY and `req` matched a different value. Since a consumer may PIN A RULE on `precise`, that is the exact false positive the designation exists to avoid. Replaced with AST evidence: collect leaves genuinely READ from a tainted source (`data.title`, `req.body.title`, `{ title }` shorthand, `fn(title)`, `x[\"title\"]`), explicitly excluding property keys, member names and binding names; a flow is `precise` only if the input's leaf is among them, else `heuristic`. Evidence is gathered from the enclosing statement so a fluent chain (`.update({…}).eq('id', data.id)`) counts as one operation. 2. IMPORTED HELPERS (medium). - The resolver did not enforce the project boundary the walker enforces, so `import '../../other-repo/db'` (or a symlink) could pull an unrelated codebase into this app's attack surface. It now rejects anything whose realpath leaves the project unless --follow-symlinks. - An imported sink kept the HELPER's line number while the endpoint reported the handler's file, so the coordinate pointed at the wrong file. Sinks reached through an import now carry their own `file`; flow linking also refuses to call such a sink `precise`, since its call site isn't visible in the handler. - `import { saveOrder as write } from './db'` looked up `write` in the target module and missed the helper. Bindings now track the exported name behind an alias. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.
What
The protect runtime is the module an edge guard imports — the Next edge-middleware template we scaffold, plus Cloudflare Workers / Deno / Bun / Supabase Functions. But
runtime.jsandrules/store.jsimportednode:fs/node:pathstatically, and the bundler rewrites those to barefs/pathindist/protect.js— specifiers that don't resolve on an edge runtime at all. Net effect: a failed edge build or an unavailable guard on exactly the platforms we advertise.Both filesystem uses are Node-only conveniences that were already fail-open, so they now load with a dynamic
await import(…):rules/store.js— the disk cache tier, behind a memoized loader. On a filesystem-less runtime it simply reports "no cache" and the memory / pluggableruleCachetiers carry last-known-good.runtime.js— the.patchstackrc.jsonapi-key fallback (resolveApiKeyis now async;createProtectionalready awaited its result).After this, the runtime graph has zero static Node-builtin imports, and the built bundle is clean.
Tests
Adds
tests/protect/edge-safe.test.ts, which pins the invariant so this can't regress:src/protect/**runtime source, anddist/protect.js(checked whendistexists — it's gitignored).The Node-path behavior is unchanged and still covered: the disk-cache / last-known-good tests (
rule-sync,runtime-pulse,runtime-guards) pass as-is. Full suite green (712), typecheck + build clean.Follow-ups (not in this PR)
A real Next-Edge / Workers build smoke test is the natural next step — this PR removes the blocker and locks the import invariant, but doesn't yet execute the guard inside an edge build.