From 8fccb99c4383b2980d94643602b08ecd6ecc8b9e Mon Sep 17 00:00:00 2001 From: luciferlive112116 <291889058+luciferlive112116@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:32:55 +0800 Subject: [PATCH] docs(selfhost): describe what the starters actually ship for critical secrets preflight.ts's KNOWN_PLACEHOLDER_SECRETS comment claimed .env.selfhost.example / .env.example "ship these EXACT literal placeholder values" for the webhook HMAC secret and the static API/MCP/internal bearer tokens. Checked both files: that was never true of either, and the real shape is worth stating precisely, since a reader was being misled about which secrets ship exploitable defaults today. Every CRITICAL_SECRET_VARS entry ships COMMENTED OUT in both files, so the hazard is what an operator finds when they uncomment one. In .env.selfhost.example all five are commented and valueless, which yields a blank -- checkCriticalSecrets skips a blank outright, and a blank cannot be a publicly known value. .env.example is the same except SELFHOST_SETUP_TOKEN, whose commented line carries change-this-long-random-value: uncommenting it, the obvious way to turn the first-run wizard on, hands the operator a published token unless this set stops them. The comment now records that, plus the fact that .env.example repeats the same literal on POSTGRES_PASSWORD's commented line, which makes it read like the house placeholder rather than one var's. On whether the check is now dead for the four that ship valueless: it is not. The set is matched against whatever the operator actually SET, not against what the files ship, so it still fires wherever that literal is pasted. It is also the ONLY check that can catch it -- at 29 characters it passes MIN_SECRET_LENGTH's 20-char bar, so the length heuristic below would wave it straight through. change-this-32-byte-random-token ships in neither starter today; noted inline and kept, since retiring a once-published placeholder buys nothing when the match is against operator input. Comment-only: no executable line changes. Closes #6285 --- src/selfhost/preflight.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/selfhost/preflight.ts b/src/selfhost/preflight.ts index 4e03dcb0c1..f82e21a375 100644 --- a/src/selfhost/preflight.ts +++ b/src/selfhost/preflight.ts @@ -77,15 +77,30 @@ function addProblem( problems.push({ var: name, message }); } -// Codex security finding: `.env.selfhost.example` / `.env.example` ship these EXACT literal placeholder -// values for high-privilege secrets (the webhook HMAC secret, plus the static API/MCP/internal bearer -// tokens). An operator who copies the starter to `.env` and misses "fill in the placeholders" runs an -// instance with a PUBLICLY KNOWN webhook secret (forgeable signatures) and PUBLICLY KNOWN bearer tokens -// (LOOPOVER_API_TOKEN bypasses app-role + per-repo write checks; INTERNAL_JOB_TOKEN gates internal -// routes) -- silently, with no error. Reject these exact strings at boot rather than trusting every -// operator to have actually edited the file. +// Codex security finding: reject the starter files' literal placeholder values at boot, rather than trusting +// every operator to have actually edited the file. An operator who copies a starter to `.env` and misses "fill +// in the placeholders" would otherwise run with a PUBLICLY KNOWN secret -- a forgeable webhook HMAC, or bearer +// tokens that bypass real checks (LOOPOVER_API_TOKEN bypasses app-role + per-repo write checks; +// INTERNAL_JOB_TOKEN gates internal routes) -- silently, with no error. +// +// What the starters ACTUALLY ship today (#6285 -- this comment used to claim both files ship literal values for +// the webhook secret and all three bearer tokens, which was never true of either). Every var below ships +// COMMENTED OUT in both files, so the hazard is what an operator finds when they uncomment one: +// - `.env.selfhost.example`: all five are commented AND valueless (`# GITHUB_WEBHOOK_SECRET=`) -- uncommenting +// yields a blank, which checkCriticalSecrets skips outright and which cannot be a publicly known value. +// - `.env.example`: same, except SELFHOST_SETUP_TOKEN's commented line carries a literal +// (`# SELFHOST_SETUP_TOKEN=change-this-long-random-value`). Uncommenting THAT -- the obvious way to turn the +// first-run wizard on -- hands the operator a published token unless this set stops them. +// That literal is also the string most likely to reach a var that ships valueless: `.env.example` repeats it on +// POSTGRES_PASSWORD's commented line, so it reads like the house placeholder rather than one var's. +// +// Load-bearing for every var here, not just the one that ships it: this set is matched against whatever the +// operator SET, not against what the files ship. And at 29 chars the literal sails past MIN_SECRET_LENGTH below, +// so this exact-match set is the ONLY check that catches it. const KNOWN_PLACEHOLDER_SECRETS = new Set([ "change-this-long-random-value", + // Ships in neither starter today. Kept as defence-in-depth: this set is matched against whatever an operator + // actually set, not against what the files ship, so retiring a once-published placeholder buys nothing. "change-this-32-byte-random-token", ]);