From 1afbc23e19f73610b02b85d407511e74815d0a7e Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Mon, 20 Jul 2026 21:52:48 +0800 Subject: [PATCH] fix: accept literal database secret punctuation --- deploy/validate-staging-database-env.sh | 32 ++++++++++++++++--- frontend/tests/database-env-validator.test.ts | 29 +++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/deploy/validate-staging-database-env.sh b/deploy/validate-staging-database-env.sh index 1fd056d1..2949219c 100755 --- a/deploy/validate-staging-database-env.sh +++ b/deploy/validate-staging-database-env.sh @@ -53,9 +53,33 @@ environment_value() { is_safe_literal() { local value="$1" - # Required values are generated as single-line hex-safe literals. This also - # permits percent-encoded schema URLs without evaluating dotenv syntax. - [[ "$value" =~ ^[A-Za-z0-9._~%:@/+,-]+$ ]] + local inner + + # Required values are literal single-line values: use an unquoted token or + # matching non-empty quotes. Dotenv interpolation, comments, and malformed + # quoting are rejected rather than evaluated, so generate secrets without $. + if [ -z "$value" ] || [[ "$value" == *'$'* ]]; then + return 1 + fi + + case "$value" in + \"*\") + inner="${value:1}" + inner="${inner%?}" + [ -n "$inner" ] && [[ "$inner" != *'"'* ]] + ;; + \'*\') + inner="${value:1}" + inner="${inner%?}" + [ -n "$inner" ] && [[ "$inner" != *"'"* ]] + ;; + *\"*|*\'*) + return 1 + ;; + *[[:space:]]*|*\#*) + return 1 + ;; + esac } require_once_non_empty() { @@ -65,7 +89,7 @@ require_once_non_empty() { count="$(definition_count "$key")" value="$(environment_value "$key")" if [ "$count" -ne 1 ] || ! is_safe_literal "$value"; then - echo "required staging database environment variable is missing or duplicated: $key" >&2 + echo "required staging database literal is missing, duplicated, or ambiguous: $key" >&2 exit 1 fi } diff --git a/frontend/tests/database-env-validator.test.ts b/frontend/tests/database-env-validator.test.ts index fbb1959b..e71ee529 100644 --- a/frontend/tests/database-env-validator.test.ts +++ b/frontend/tests/database-env-validator.test.ts @@ -137,6 +137,35 @@ test("database env validator rejects quoted and interpolated required secrets", } }); +test("database env validator accepts punctuated literal required secrets", () => { + const root = mkdtempSync(join(tmpdir(), "jyotisha-database-env-")); + const envFile = join(root, ".env.staging.database"); + + try { + for (const password of ["c2VjcmV0IT0=", '"secret!=value"']) { + writeFileSync( + envFile, + `${validEnvironment + .map((line) => + line.startsWith("POSTGRES_PASSWORD=") + ? `POSTGRES_PASSWORD=${password}` + : line, + ) + .join("\n")}\n`, + { mode: 0o600 }, + ); + chmodSync(envFile, 0o600); + + const result = spawnSync("bash", [validator, envFile], { encoding: "utf8" }); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, "staging database environment validated\n"); + assert.doesNotMatch(`${result.stdout}${result.stderr}`, /secret!=value|c2VjcmV0IT0=/); + } + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test("database env validator rejects symlinks and unsafe modes", () => { const root = mkdtempSync(join(tmpdir(), "jyotisha-database-env-")); const envFile = join(root, ".env.staging.database");