Skip to content
Code Recycle

Component · for humans & their agents

Next.js Server Prop Leak Verdict

verified · first-partyactively maintained$0 during beta (was $59)

Your API token is in the page source and you never wrote NEXT_PUBLIC_ anywhere. Measured: the client component never even reads the field, and scanning your JS bundle does not find it.

by Code Recycle

Get it free — beta

Every claim on this page is refundable if it is untrue — refund policy.

Verified: 25 tests

Decides whether a prop crossing the SERVER -> CLIENT boundary puts data into the HTML a browser receives. Pure function over the shape of what you pass: no I/O, no React or Next.js dependency.

Decides whether a prop crossing the SERVER -> CLIENT boundary puts data into the HTML a browser receives. Pure function over the shape of what you pass: no I/O, no React or Next.js dependency.

Built for React Server Components / Next.js App Router (MIT). Not affiliated with or endorsed by Vercel.

THE MEASUREMENT. Next 15.5.22, production build, read over the wire. A server component reads env vars with NO NEXT_PUBLIC_ prefix, builds an ordinary object, and passes it to a client component:

  // the CLIENT component
  "use client";
  export default function Widget({ user, config }) {
    return <div>widget for {user.name} region {config.region}</div>;
  }
  // the SERVER component
  const user = { id: 7, name: "Ada", apiToken: process.env.INTERNAL_API_TOKEN };
  return <Widget user={user} config={{ region: process.env.REGION }} />;

What a browser receives from GET /leak:

  {"user":{"id":7,"name":"Ada","apiToken":"SECRET_TOKEN_AAA111"},...

The token is in the page source, verbatim.

THREE THINGS MAKE THIS DIFFERENT FROM THE NEXT_PUBLIC_ MISTAKE.

1. NO PREFIX IS INVOLVED. Searching the whole app source for NEXT_PUBLIC returns one hit, and it is the comment saying there isn't one. The variable is INTERNAL_API_TOKEN.

2. THE CLIENT COMPONENT NEVER READS THE FIELD. It renders user.name. Whether a field is used has no bearing on whether it is serialized -- the whole prop crosses. "It's not used on the client" is the reasoning that keeps this in place, and it is not a protection.

3. IT IS NOT IN ANY JS BUNDLE:

     grep -rl SECRET_TOKEN_AAA111 .next/static/   ->  (no matches)
     grep -rl SECRET_TOKEN_AAA111 .next/          ->  .next/server/app/leak.html

SCANNING YOUR CLIENT BUNDLE FOR SECRETS DOES NOT CATCH THIS. The leak is in the RSC payload embedded in the HTML, not in the JavaScript. A green secret-scan means the scanner looked somewhere the value never was.

THE CONTROL HOLDS: a secret read on the server and NEVER PASSED does not appear in the HTML. So this is not "server env leaks into pages" -- it is precisely the prop boundary, which is why a rule about env var naming cannot catch it.

THE ONE THAT GETS WORSE OVER TIME. Passing a database row, ORM entity, session or API response straight through is reported on its SHAPE ALONE, even when you are confident nothing sensitive is in it today -- that confidence is about today's columns. Nobody enumerated the field set, so the next migration that adds a column adds it to the page source too, with no change in that file, nothing flagged in review, and no test failing.

THE REMEDY, MEASURED: pass the fields the client needs, not the object you happen to be holding. <Widget user={{ name: user.name }} /> took the token from ONE occurrence in the page to ZERO, with no other change. Picking fields also bounds the payload against future columns -- the part a one-time audit cannot do.

IT STAYS QUIET WHERE IT SHOULD: explicitly picked fields return SAFE with the reason, so you can stop thinking about that call site. IT REFUSES rather than guesses on an unknown construction -- but does NOT refuse on a passed-through row of unknown contents, because for that shape "unknown" is the normal state and the finding already stands.

VERIFIED: 25 tests, measured by running the suite, every mutation observed FAILING before restore. One test caught a real gap in the source -- a citation that did not name the version it rested on.

DELIVERY: signed download of a hash-verified tarball, immediately on purchase. Permissive licence: unlimited products, unlimited clients, unlimited seats, no attribution, perpetual and irrevocable. One restriction, do not republish the source as source.

Interface

What you call, and what comes back. Types and signatures only — the implementation ships with the source.

  export function evaluateCrossingProp(prop: CrossingProp): Verdict;
  export type Verdict = | { status: "SAFE";

01Capabilities

Does

  • + Security triage
  • + Compliance audit
  • + Data exposure boundary

Doesn’t

  • No exclusions declared

02Requirements & stack

Depends on

No declared dependencies

Credentials needed

None declared

Stack

typescript

03Community

No endorsements yet

No verified confirmations yet — be the first.

Confirmations come from verified purchasers, installers, vetted reviewers, or an installation outcome your org reported through the agent tools. They grade quality — security is verified separately, and community votes can never override the security gate.

Open an issue

Sign in to confirm — weight comes from verified usage, not vote count.

0 open · 0 answered · 0 fixed · 1 said it worked

04Trust Passport

Full passport →
–/100

0/0 automated components pass. An automated score is never a security guarantee.

✓ Verified · first-partyreviewed Sep 20, 2026 · re-verification due Dec 19, 2026
  • publisher identity Publisher status verified; 1 verification(s) on file
  • malicious pattern scan No known malicious-behavior patterns across 15 source file(s) plus listing text
  • capability contract All 1 observed capability reference(s) match the declared manifest (1 declared as cited source(s), not contacted)
  • agent safety scan No injection patterns in agent-readable content
  • provenance No release signature or provenance attestation
  • behavioral sandbox Not performed in this environment — requires the production isolated runner (docs/sandbox-requirements.md). No untrusted code is ever executed on the application host.

Every listing must pass this review before it can be sold, and it is re-run on every release. Verification describes what we checked — it is not a guarantee that the software is safe.

VersionChannelReleasedNotes
1.0.0stableAug 6, 2026First public release.