fix(plugin): renderNow fails closed without a token; v0.32.0 - #68
Conversation
`isRenderNowAuthorized` treated "no token configured" as "authorize anyone who
sends the header":
return token ? value === token : true;
The schema documented that as an intentional unauthenticated mode, warned about at
config-apply time. On a path that takes public crawler traffic it is a DoS lever,
not a convenience: an authorized GET can skip the served cache and force a
synchronous render that occupies the request for up to `timeoutMs` (default 30s).
It is also the state a misconfiguration lands in, which is what makes the
permissive reading dangerous rather than merely lax. `config.js` assigns from the
environment only when the variable is actually set:
if (renderNowEnv && process.env[renderNowEnv]) { ... }
so `valueEnv` naming a variable that is unset — a typo, or an env that was never
provisioned — leaves `token` at its empty default. Under the old reading that typo
silently became an open door, and the failure mode of a secret-management mistake
should never be "no secret required".
Now: enabling is necessary but not sufficient. Without a non-empty token the
levers are inert, and the config warning says so — it previously reported a "DoS
risk" that can no longer occur, which would send someone hunting an exposure that
does not exist. The warning also names an unresolved `valueEnv` variable, which is
the difference between a five-second fix and a hunt.
BREAKING for anyone deliberately running enabled-with-no-token to leave the levers
open to any caller. That configuration was documented as a DoS vector and warned
about, so it is being withdrawn rather than preserved; such deployments must now
set `renderNow.token` (or a `valueEnv` that resolves) to keep the levers working.
The three behavioral tests were each verified to fail with the change reverted, so
they guard the semantics rather than merely restating them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request updates the on-demand rendering feature (renderNow) to fail closed when no token is configured, eliminating a potential DoS risk where the feature previously defaulted to unauthenticated. It updates the configuration schema, authorization logic, warning messages, and tests to reflect this secure-by-default behavior. The reviewer suggests refining the warning message in config.js to explicitly state that the feature is disabled when a token is missing, ensuring the logs accurately mirror the runtime gate logic.
| valueEnv | ||
| ? `renderNow.enabled but renderNow.valueEnv ("${valueEnv}") is not set in the environment and no renderNow.token is configured — the on-demand levers stay INERT (they fail closed rather than authorizing anyone)` | ||
| : 'renderNow.enabled but no renderNow.token is configured — the on-demand levers stay INERT (they fail closed rather than authorizing anyone); set renderNow.token or renderNow.valueEnv' |
There was a problem hiding this comment.
Ensure configuration warning or validation logs accurately mirror the runtime gate logic. If a feature is disabled due to a missing or invalid dependent configuration, the logs should report it as disabled rather than claiming it is enabled. Please update the warning message to clearly state that renderNow is disabled.
| valueEnv | |
| ? `renderNow.enabled but renderNow.valueEnv ("${valueEnv}") is not set in the environment and no renderNow.token is configured — the on-demand levers stay INERT (they fail closed rather than authorizing anyone)` | |
| : 'renderNow.enabled but no renderNow.token is configured — the on-demand levers stay INERT (they fail closed rather than authorizing anyone); set renderNow.token or renderNow.valueEnv' | |
| valueEnv | |
| ? `renderNow is disabled because renderNow.valueEnv ("${valueEnv}") is not set in the environment and no renderNow.token is configured.` | |
| : 'renderNow is disabled because no renderNow.token is configured. Set renderNow.token or renderNow.valueEnv.' |
References
- Ensure configuration warning or validation logs accurately mirror the runtime gate logic. If a feature is disabled due to a missing or invalid dependent configuration, the logs should report it as disabled rather than claiming it is enabled.
There was a problem hiding this comment.
Taken — the principle is right, a warning must not read as though the feature is running.
I kept the enabled is true but clause rather than dropping to just "renderNow is disabled because...", because both facts matter to whoever is reading the log: that nothing is on, and why the enabled: true they wrote did not take effect. Someone who set the flag and then reads "renderNow is disabled" with no reference to their setting is likely to go check whether the flag took, which is the hunt the message should prevent.
So:
renderNow.enabled is true but no renderNow.token is configured — renderNow is DISABLED
(the levers fail closed rather than authorizing anyone); set renderNow.token or renderNow.valueEnv
and with an unresolved variable named:
renderNow.enabled is true but renderNow.valueEnv ("FOO") is not set in the environment and no
renderNow.token is configured — renderNow is DISABLED (the levers fail closed rather than
authorizing anyone)
Also aligned the schema description to say DISABLED rather than "inert", so the docs and the log use one vocabulary. Tests updated to assert the wording. 389/389 pass.
…time gate Review feedback: a config warning must not read as though the feature is running. The message said "renderNow.enabled but ... stay INERT", which named the contradiction but left the reader to infer the state. It now says renderNow is DISABLED outright, while keeping "enabled is true but" so the contradiction is still diagnosable — someone who set enabled: true needs to see why that did not take effect, not just that something is off. Aligned the schema description to the same word so the docs and the log agree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to a security-medium review finding on the consumer side (
kohls-pr#49). The consumer fix there was to setenabled: false; this removes the underlying footgun for every consumer.The problem
isRenderNowAuthorizedtreated "no token configured" as "authorize anyone who sends the header":The schema documented this as an intentional unauthenticated mode, warned about at config-apply time. On a path that takes public crawler traffic that is a DoS lever, not a convenience — an authorized GET can skip the served cache and force a synchronous render that occupies the request for up to
timeoutMs(default 30s).Why permissive is the wrong default here
It is the state a misconfiguration lands in.
config.jsassigns from the environment only when the variable is actually set:So
valueEnvnaming a variable that is unset — a typo, or an environment that was never provisioned — silently leavestokenat its empty default. Under the old reading, a typo in a variable name became an open door. The failure mode of a secret-management mistake should never be "no secret required."This is not hypothetical: the review suggestion that prompted this was to replace a hardcoded token with
valueEnv, and the variable in question was not provisioned anywhere in that deployment. Applying it as suggested would have swapped a guessable token for no token at all — which, under the old semantics, was strictly worse.The change
isRenderNowAuthorizedrequires a non-empty token. Enabling is now necessary but not sufficient; presence of the header never authorizes on its own.tokenis described as required, an empty one as making the feature inert, and a guessable placeholder ("true") called out as no better than none.valueEnvvariable, which is the difference between a five-second fix and a hunt.Breaking change
Anyone deliberately running enabled-with-no-token to leave the levers open to any caller loses that. It was documented as a DoS vector and warned about at startup, so it is being withdrawn rather than preserved. Such deployments must set
renderNow.token(or avalueEnvthat resolves) to keep the levers working. Nothing changes for a deployment that already sets a token, and nothing changes for organic crawler traffic either way — it never carried the header.Tests
389/389pass; lint and format clean.Three behavioral tests, each verified to fail with the change reverted so they guard the semantics rather than restate them:
isRenderNowAuthorizedfails closed with no token (previously the case that authorized on presence)valueEnvleaves the levers inert, not openresolveServingPolicygrants no levers when the token is missing — the end-to-end consequence: a miss proxies the origin andCache-Controlis not honored as a cache skipPlus warning coverage for both branches (with and without
valueEnv), and that a configured token reports nothing.Notes
Version 0.32.0.
0.31.0is left for #66, which still stamps0.29.0and needs a re-stamp sincemainis now at0.30.0.🤖 Generated with Claude Code