Skip to content

Stack-trace policy defaults from ambient NODE_ENV, so a production build leaks install paths on every error when NODE_ENV=development #3221

Description

@frenzzy

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

  1. 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.
  2. 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.
  3. 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");
});

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions