You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Related to #38443, but a different signature. In that report Deno.env.get() returns empty. In mine the store returns a previously-set value instead of the one I wrote, and delete-and-re-add does not fix it.
Sequence, all verified against the deployed function rather than the dashboard:
Set RPT_CRON_SECRET via dashboard. Function reports it configured, but the comparison against my value fails.
Added a diagnostic to the function: it holds a 66-char value; I set a 64-char value. SHA-256 prefixes differ — e9d31de5 held vs 2bd55afa sent.
supabase secrets unset RPT_CRON_SECRET — confirmed via the function that it went unconfigured.
supabase secrets set with a fresh 64-char value, then redeployed.
The old 66-char value came back — same fingerprint e9d31de5 as before the unset.
So the delete is observed to take effect, but the next read restores a prior value rather than the one written.
Boot log confirms the key name is exact, no whitespace:
non-SUPABASE_ env var NAMES: ["DENO_DEPLOYMENT_ID","DENO_REGION","RPT_CRON_SECRET","SB_EXECUTION_ID","SB_REGION"]
Ruled out on my side: header stripping (header arrives intact, fingerprint matches local), dotenv parsing (reproduced with raw curl), eight value transformations (quoting, case, leading/trailing whitespace), and the comparison logic itself (the sent-value fingerprint matches .env exactly).
Full write-up with the fingerprint table below.
Bug report — Edge Function secret store serves a stale value after unset + set
supabase secrets set RPT_CRON_SECRET=<value> reports success, but the value
returned by Deno.env.get('RPT_CRON_SECRET') inside the deployed function does
not change. A previously-stored value persists across unset, set, and functions deploy.
The stale value is 66 characters. Every value we have attempted to store is 64 characters. The function reads the correct variable name and the variable
is present.
What we can prove
The function was instrumented to report, on a 401, the length and a fingerprint (first 4 bytes of SHA-256, hex) of the presented header and of Deno.env.get('RPT_CRON_SECRET'). No secret values are logged or returned.
Environment variable name is exactly correct
Boot log, Object.keys(Deno.env.toObject()) filtered to non-SUPABASE_ names, JSON.stringifyd so surrounding whitespace in a key would be visible:
[geocode-worker] non-SUPABASE_ env var NAMES:
["DENO_DEPLOYMENT_ID","DENO_REGION","RPT_CRON_SECRET","SB_EXECUTION_ID","SB_REGION"]
"RPT_CRON_SECRET" — no leading or trailing whitespace in the key. This rules
out the key-whitespace variant reported in community discussion #38443.
The stored value does not change
Step
cron_secret_configured
cron_secret_len
cron_secret_fp
Initial state
true
66
e9d31de5
After secrets unset RPT_CRON_SECRET
false
0
—
After secrets set (fresh 64-char value) + functions deploy
true
66
e9d31de5
The fingerprint after the reset is identical to the fingerprint before it.
The unset was observed to take effect (configured went to false), so the
store is not simply ignoring the delete — it restores a prior value on the next
read.
The value we are sending is correct and intact
cron_header_len
64
cron_header_fp
2bd55afa
2bd55afa matches a SHA-256 fingerprint computed locally over the exact value
in our .env, so the client sends the intended bytes and they survive the
gateway unchanged.
The stored value is not a transformation of ours
We computed fingerprints for every two-character wrapping of our 64-character
value and compared against e9d31de5:
Candidate
Length
Fingerprint
raw
64
2bd55afa
"value"
66
72d203f2
value + CRLF
66
76e055c2
value
66
1104c281
backticks
66
58eb2c3c
value + two spaces
66
f743313c
(value)
66
9b296a7b
None match. The stored value is not our value with quotes, whitespace or line
endings attached — it is a different secret.
Reproduction
Deploy a function that reads Deno.env.get('MY_SECRET') and reports its
length and a hash prefix (never the value).
supabase secrets set MY_SECRET=<64-char value A>
supabase functions deploy <fn> — observe length and fingerprint.
supabase secrets set MY_SECRET=<different 64-char value B>
supabase functions deploy <fn> — observe the value from step 3, not B.
Set both via the CLI (npx supabase secrets set "MY_SECRET=..." on Windows
PowerShell) and, earlier, via the dashboard. Same outcome from both paths.
Impact
The secret gates the scheduled caller of a function that runs with the
service-role key. Because the stored value cannot be updated, the scheduled
path cannot be enabled at all — the function correctly refuses every call
presenting the intended secret.
The failure is silent from the outside: the refusal is a plain 401, identical
to a genuinely unauthorized request. We only isolated it after adding
length-and-fingerprint reporting to the function. A deployment whose secret
silently reverts looks exactly like a security boundary working correctly,
which we would suggest is the more serious property of this bug.
What we are not claiming
We have not identified the source of the 66-character value. It may predate
our first set; we did not fingerprint the store before the first write.
We have not tested whether other secret names are affected on this project.
We have not tested on another project or region.
Workaround in use
The scheduled caller is disabled. The same function is driven by an
authenticated user session instead, which uses a different auth path (a real
user JWT, verified with auth.getUser) and is unaffected.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Related to #38443, but a different signature. In that report Deno.env.get() returns empty. In mine the store returns a previously-set value instead of the one I wrote, and delete-and-re-add does not fix it.
Sequence, all verified against the deployed function rather than the dashboard:
Set RPT_CRON_SECRET via dashboard. Function reports it configured, but the comparison against my value fails.
Added a diagnostic to the function: it holds a 66-char value; I set a 64-char value. SHA-256 prefixes differ — e9d31de5 held vs 2bd55afa sent.
supabase secrets unset RPT_CRON_SECRET — confirmed via the function that it went unconfigured.
supabase secrets set with a fresh 64-char value, then redeployed.
The old 66-char value came back — same fingerprint e9d31de5 as before the unset.
So the delete is observed to take effect, but the next read restores a prior value rather than the one written.
Boot log confirms the key name is exact, no whitespace:
non-SUPABASE_ env var NAMES: ["DENO_DEPLOYMENT_ID","DENO_REGION","RPT_CRON_SECRET","SB_EXECUTION_ID","SB_REGION"]
Runtime: supabase-edge-runtime-1.74.4 (Deno v2.1.4).
Ruled out on my side: header stripping (header arrives intact, fingerprint matches local), dotenv parsing (reproduced with raw curl), eight value transformations (quoting, case, leading/trailing whitespace), and the comparison logic itself (the sent-value fingerprint matches .env exactly).
Full write-up with the fingerprint table below.
Bug report — Edge Function secret store serves a stale value after unset + set
Filed by: Devin Zapata · Date: 2026-09-03
Project ref:
xlwdyomuoufibaudfhkgRuntime:
supabase-edge-runtime-1.74.4(Deno v2.1.4)Function:
geocode-worker, deployment version 6Summary
supabase secrets set RPT_CRON_SECRET=<value>reports success, but the valuereturned by
Deno.env.get('RPT_CRON_SECRET')inside the deployed function doesnot change. A previously-stored value persists across
unset,set, andfunctions deploy.The stale value is 66 characters. Every value we have attempted to store is
64 characters. The function reads the correct variable name and the variable
is present.
What we can prove
The function was instrumented to report, on a
401, the length and afingerprint (first 4 bytes of SHA-256, hex) of the presented header and of
Deno.env.get('RPT_CRON_SECRET'). No secret values are logged or returned.Environment variable name is exactly correct
Boot log,
Object.keys(Deno.env.toObject())filtered to non-SUPABASE_names,JSON.stringifyd so surrounding whitespace in a key would be visible:"RPT_CRON_SECRET"— no leading or trailing whitespace in the key. This rulesout the key-whitespace variant reported in community discussion #38443.
The stored value does not change
cron_secret_configuredcron_secret_lencron_secret_fptruee9d31de5secrets unset RPT_CRON_SECRETfalsesecrets set(fresh 64-char value) +functions deploytruee9d31de5The fingerprint after the reset is identical to the fingerprint before it.
The unset was observed to take effect (
configuredwent tofalse), so thestore is not simply ignoring the delete — it restores a prior value on the next
read.
The value we are sending is correct and intact
cron_header_lencron_header_fp2bd55afa2bd55afamatches a SHA-256 fingerprint computed locally over the exact valuein our
.env, so the client sends the intended bytes and they survive thegateway unchanged.
The stored value is not a transformation of ours
We computed fingerprints for every two-character wrapping of our 64-character
value and compared against
e9d31de5:2bd55afa"value"72d203f276e055c2value1104c28158eb2c3cf743313c(value)9b296a7bNone match. The stored value is not our value with quotes, whitespace or line
endings attached — it is a different secret.
Reproduction
Deno.env.get('MY_SECRET')and reports itslength and a hash prefix (never the value).
supabase secrets set MY_SECRET=<64-char value A>supabase functions deploy <fn>— observe length and fingerprint.supabase secrets unset MY_SECRET— observeundefined/ length 0.supabase secrets set MY_SECRET=<different 64-char value B>supabase functions deploy <fn>— observe the value from step 3, not B.Set both via the CLI (
npx supabase secrets set "MY_SECRET=..."on WindowsPowerShell) and, earlier, via the dashboard. Same outcome from both paths.
Impact
The secret gates the scheduled caller of a function that runs with the
service-role key. Because the stored value cannot be updated, the scheduled
path cannot be enabled at all — the function correctly refuses every call
presenting the intended secret.
The failure is silent from the outside: the refusal is a plain
401, identicalto a genuinely unauthorized request. We only isolated it after adding
length-and-fingerprint reporting to the function. A deployment whose secret
silently reverts looks exactly like a security boundary working correctly,
which we would suggest is the more serious property of this bug.
What we are not claiming
our first
set; we did not fingerprint the store before the first write.Workaround in use
The scheduled caller is disabled. The same function is driven by an
authenticated user session instead, which uses a different auth path (a real
user JWT, verified with
auth.getUser) and is unaffected.All reactions