host: JOLT_BAKE_ENV_ALLOWLIST scrubs the env during image bakes (jolt-s3j)

A native-executable build bakes the jolt ctx, and env-reading libraries
(config.core/load-env) snapshot the ENTIRE build environment into it — jpm
marshals that into the binary. GitHub push protection caught real API
tokens inside an example's build output this way.

With JOLT_BAKE_ENV_ALLOWLIST set (comma-separated names — a project's
build.sh exports it for the bake), System/getenv serves only the listed
variables: single-name reads of unlisted vars return nil and the full
snapshot is filtered. Unset — every normal runtime — reads stay live and
unfiltered, so a baked binary that re-reads env at startup (config.core/
reload-env) sees the real runtime environment.

Verified A/B on ring-app: a planted token appears in the unscrubbed binary
(strings | grep: 1 hit) and not in the scrubbed one (0), which still serves.
Direct janet.os/environ bridge calls remain unfiltered host access, as
documented.
This commit is contained in:
Yogthos 2026-06-11 21:50:45 -04:00
parent d8260fb587
commit 59853492dd
2 changed files with 33 additions and 1 deletions

View file

@ -186,3 +186,16 @@
["replace fn gets group vector" "\"v=k\""
"(do (require (quote [clojure.string :as s9])) (s9/replace \"k=v\" #\"(\\w+)=(\\w+)\" (fn [[_ k v]] (str v \"=\" k))))"]
["indexOf int needle is a char code" "1" "(.indexOf \"a=b\" 61)"])
# JOLT_BAKE_ENV_ALLOWLIST (jolt-s3j): with the allowlist set, System/getenv
# serves only the listed names — so an image bake can't marshal the builder's
# secrets into the binary. Unset, reads are live and unfiltered.
(defspec "host-interop / bake env scrub"
["unlisted name reads nil under the allowlist" "nil"
"(do (janet.os/setenv \"JOLT_BAKE_ENV_ALLOWLIST\" \"PATH\") (let [r (System/getenv \"HOME\")] (janet.os/setenv \"JOLT_BAKE_ENV_ALLOWLIST\" nil) r))"]
["listed name still reads" "true"
"(do (janet.os/setenv \"JOLT_BAKE_ENV_ALLOWLIST\" \"HOME\") (let [r (System/getenv \"HOME\")] (janet.os/setenv \"JOLT_BAKE_ENV_ALLOWLIST\" nil) (string? r)))"]
["full snapshot filtered to the allowlist" "true"
"(do (janet.os/setenv \"JOLT_BAKE_ENV_ALLOWLIST\" \"HOME\") (let [e (System/getenv)] (janet.os/setenv \"JOLT_BAKE_ENV_ALLOWLIST\" nil) (and (contains? (set (keys e)) \"HOME\") (= 1 (count (keys e))))))"]
["no allowlist: unfiltered live reads" "true"
"(string? (System/getenv \"HOME\"))"])