Summary
Error-stack serialization defaults from the ambient NODE_ENV, not from the build's own DEV flag. A production artifact running with NODE_ENV=development therefore emits stack traces — and because the sanitizer's own generic Error carries a stack, this happens on every server error, not only on errors an author marked with markSafeError.
The disclosure is the deployment's absolute filesystem path.
Measured
next @ ee73e053, production build, a server function that throws new Error("real db error with secret hunter2"):
NODE_ENV="production" status=500 stackInBody=false absPathLeak=false secretLeak=false
NODE_ENV="development" status=500 stackInBody=true absPathLeak=true secretLeak=false
NODE_ENV=undefined status=500 stackInBody=false absPathLeak=false secretLeak=false
The leaked frame is the sanitizer's own:
at sanitizeServerError (file:///…/packages/web/server-functions/dist/server.js:1581:10)
Two things worth stating plainly, because they bound this:
- The application secret does not leak. The message is replaced with the generic one first;
secretLeak=false in every row. What leaks is the install path and the runtime's internal frames.
NODE_ENV unset is fail-safe, which is the common container default.
Where
packages/web/serialization/src/serializer.ts:128-135, from bacfb343 feat: serializeErrorStacks codec option pins stack policy to the deployment (#3152).
That commit added serializeErrorStacks precisely so the policy could be pinned, and the option works — setting it explicitly overrides the default in both directions, which I verified. This issue is only about what the default reads.
Why the default matters
NODE_ENV=development against a production bundle is a misconfiguration, but it is a common one — a stray value in a compose file or a CI-inherited env. The build already knows what it is: DEV is compiled in. Reading the policy from an ambient string means a correctly-built production server can be talked into a dev-only behaviour by an environment variable that has no other effect on that artifact.
The documented framing is also narrower than the behaviour: it describes the exposure as application-code stacks for errors marked safe. The sanitized generic error carries one too, so any caller who can trigger any error learns the path.
Options
- Default
serializeErrorStacks from the build's own DEV flag rather than process.env.NODE_ENV. The artifact then behaves the same wherever it runs, and the explicit option still overrides.
- Keep the
NODE_ENV default but strip the sanitizer's own frames, so the generic error never carries a stack regardless of policy. Narrower, and arguably right on its own merits — a stack that only points at Solid's internals helps no one debug their application.
- Document that a production deployment must not set
NODE_ENV=development.
(1) and (2) close different halves and are not exclusive: (1) makes the artifact self-consistent, (2) makes the generic error carry nothing worth having.
Regression test
it("keeps a production build silent about stacks whatever NODE_ENV says", async () => {
process.env.NODE_ENV = "development";
const body = await (await handleServerFunctionRequest(postThatThrows(id))).text();
expect(body).not.toContain("stack");
});
Summary
Error-stack serialization defaults from the ambient
NODE_ENV, not from the build's ownDEVflag. A production artifact running withNODE_ENV=developmenttherefore emits stack traces — and because the sanitizer's own genericErrorcarries a stack, this happens on every server error, not only on errors an author marked withmarkSafeError.The disclosure is the deployment's absolute filesystem path.
Measured
next@ee73e053, production build, a server function that throwsnew Error("real db error with secret hunter2"):The leaked frame is the sanitizer's own:
Two things worth stating plainly, because they bound this:
secretLeak=falsein every row. What leaks is the install path and the runtime's internal frames.NODE_ENVunset is fail-safe, which is the common container default.Where
packages/web/serialization/src/serializer.ts:128-135, frombacfb343feat: serializeErrorStacks codec option pins stack policy to the deployment (#3152).That commit added
serializeErrorStacksprecisely so the policy could be pinned, and the option works — setting it explicitly overrides the default in both directions, which I verified. This issue is only about what the default reads.Why the default matters
NODE_ENV=developmentagainst a production bundle is a misconfiguration, but it is a common one — a stray value in a compose file or a CI-inherited env. The build already knows what it is:DEVis compiled in. Reading the policy from an ambient string means a correctly-built production server can be talked into a dev-only behaviour by an environment variable that has no other effect on that artifact.The documented framing is also narrower than the behaviour: it describes the exposure as application-code stacks for errors marked safe. The sanitized generic error carries one too, so any caller who can trigger any error learns the path.
Options
serializeErrorStacksfrom the build's ownDEVflag rather thanprocess.env.NODE_ENV. The artifact then behaves the same wherever it runs, and the explicit option still overrides.NODE_ENVdefault but strip the sanitizer's own frames, so the generic error never carries a stack regardless of policy. Narrower, and arguably right on its own merits — a stack that only points at Solid's internals helps no one debug their application.NODE_ENV=development.(1) and (2) close different halves and are not exclusive: (1) makes the artifact self-consistent, (2) makes the generic error carry nothing worth having.
Regression test