Repository navigation
No published default secret: read SESSION_SECRET, else a random key per process (P1-11) - #9
Merged
eliasjpr merged 5 commits intoOct 27, 2026
Conversation
…cess (P1-11) Without a configured secret, sessions were signed with a key published in this repository, and the production refusal read CRYSTAL_ENV once when the shard loaded, as an exact lowercase match. - secret defaults to SESSION_SECRET, else a random key for this process with one warning; InsecureSecretException where require_secure_secret holds. - require_secure_secret, unless set, reads CRYSTAL_ENV (case-insensitive) each time it is checked. - Docs, UPGRADE and the example no longer show hard-coded fallback keys. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HT5gvYGR83U2M4DyA6NA6f
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| CodeStyle | 1 minor |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
…irst use raises) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HT5gvYGR83U2M4DyA6NA6f
- `secret` no longer memoises the random fallback, so setting CRYSTAL_ENV to production after it was used is still refused. - `validate_secret!` refuses a secret under 32 bytes (unless use_kdf) with InsecureSecretException, instead of an ArgumentError from AES that the handler logged and swallowed; SESSION_SECRET=short and secret = "" are covered. - SessionHandler validates the secret before loading when its store encrypts (CookieStore, or encrypt_redis_data): a misconfigured production app now fails each request with InsecureSecretException (a 500) instead of answering 200 without a working session. - Docs: no hard-coded fallback secrets left (quick start, installation, clustered Redis store); snippets no longer read ENV["SESSION_SECRET"] or ENV.fetch (a KeyError when unset), they rely on the default; the behaviour without a secret is described as it is; the Azu page says how Azu::Env.secret differs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HT5gvYGR83U2M4DyA6NA6f
…e the encryptor The HMAC signature uses the raw secret, so PBKDF2 does not make a short one safe: the production preset accepted "", "a" and "abc". The 32-byte minimum now holds whatever use_kdf says. Session.configure validates the secret when the configured store encrypts with it (CookieStore, or encrypt_redis_data), so a misconfigured app fails at boot. SessionHandler keeps its per-request check as a backstop; both ask the store (Store#encrypts_with_secret?). Configuration#encryptor reuses its encryptor while the secret, digest and KDF settings are unchanged, so PBKDF2 no longer runs on every encrypt and decrypt. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HT5gvYGR83U2M4DyA6NA6f
key-derivation.md no longer says PBKDF2 protects weak or short secrets, and says when the key is derived. UPGRADE.md: the short-secret bullet no longer exempts use_kdf, the boot-time check is listed, and the require_secure_secret sentence is its own bullet. security.md: random secret outside production and staging. encryption.md: stale comments. project.mdc: no hard-coded secret, the store API and Session::Base. The Azu page names AZU_STRICT_ENVS. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HT5gvYGR83U2M4DyA6NA6f
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
config.secretdefaults toSESSION_SECRET. Without it:Session::InsecureSecretExceptionwhererequire_secure_secretholds, and a random key for this process (one warning) elsewhere.config.secret = nilgoes back to that default. The random fallback is not memoised per configuration, so it is refused again as soon asrequire_secure_secretholds (e.g.CRYSTAL_ENV=productionset later).require_secure_secret, unless set explicitly, readsCRYSTAL_ENV(case-insensitive, trimmed) each time it is checked, instead of once when the shard loads. Production and staging, as before.InsecureSecretExceptionwith a clear message, instead of anArgumentErrorfrom AES. This holds withuse_kdfon too (the:production,:high_securityand:clusteredpresets): the HMAC signature uses the raw secret (Message::Verifier), so PBKDF2 does not make a short secret safe.Session.configurevalidates the secret when the configured store encrypts with it (Store#encrypts_with_secret?:CookieStore, orencrypt_redis_data), so a misconfigured production app fails at boot instead of answering every request, liveness probes included, with a 500.Configuration#encryptorreuses its encryptor while the secret, digest and KDF settings are unchanged (one reference holding the settings tuple and the encryptor), so PBKDF2 (100k iterations) no longer runs on every encrypt and decrypt.SessionHandlerstill validates the secret before loading when its store encrypts, as a backstop for handlers built withoutSession.configure: such an app fails each request withInsecureSecretException(a 500, logged by the server) instead of answering 200 without a working session. Stores that do not encrypt never read the secret and are unaffected.Configuration::DEFAULT_SECRETstays (so code that names it still compiles) and is still refused whererequire_secure_secretholds; it is never used by default.examples/configuration_examples.crdrop every hard-coded fallback key, and snippets no longer readENV["SESSION_SECRET"]/ENV.fetch("SESSION_SECRET")(aKeyErrorwhen unset): they rely on the default. The Azu page says how this differs fromAzu::Env.secret(azu refuses the fallback in itsstrict_environments,productionunlessAZU_STRICT_ENVSwidens it; session in production and staging).Why (gate-2 audit P1-11)
An app that never set a secret signed its cookies with a key published in this repository, so anyone could forge a
UserSession. The refusal covered onlyCRYSTAL_ENVexactlyproduction/stagingat load time:Production, or aCRYSTAL_ENVset after the shard was required, ran on the public key.Behaviour change
SESSION_SECRET, sessions now end when the process restarts (the key is random per process). SetSESSION_SECRETto keep them.use_kdf(with the:productionpreset,"","a"and"abc"were accepted before),InsecureSecretException(before, encryption failed with anArgumentErrorthe handler swallowed, so such a setup never had working cookie sessions).encrypt_redis_data) and no usable secret in production/staging,Session.configureraises at boot; a handler built without it fails requests with a 500 instead of a 200 with no session. Set the secret (orSESSION_SECRET) before, or inside, theSession.configureblock that sets such a store.Tests
spec/configuration_spec.cr(written first): the default is never the published secret and is the same for every configuration in a process;SESSION_SECRETis read;CRYSTAL_ENV=Productionset after the configuration was built makesencryptorraise, also after the random fallback was already used;SESSION_SECRET=shortandconfig.secret = ""raiseInsecureSecretException("at least 32 bytes");SessionHandlerwith a cookie store and a short secret raises. The four follow-up examples failed before the change. Two existing specs used a 31-byte secret and now use 32 bytes.encryptoris reused while settings are unchanged and rebuilt when the salt or secret changes;validate_secret_for!raises for aCookieStoreand not for aMemoryStore;Session.configureraises at boot withencrypt_redis_dataand a short secret, and under the production preset; it does not raise when the store does not encrypt.spec/encryptor_spec.crstill buildsMessage::Encryptordirectly with short secrets anduse_kdf(the class itself has no minimum; only the configuration enforces it).crystal specwith a local Redis: 539 examples, 0 failures (Crystal 1.22.1).crystal tool format --check src spec scripts examples: clean.scripts/check_docs.cr(README, UPGRADE, docs): 157 snippets, 0 failed.Semver: patch or minor at your choice; no minor/major release marker in the commits, so the release job makes a patch.
Generated by Claude Code