Skip to content

Refuse a schema default that holds a password, and never print one (BL-209) - #98

Merged
rabbilyasar merged 1 commit into
mainfrom
rabbilyasar/bl-209-credential-default-gate
Oct 28, 2026
Merged

rabbilyasar merged 1 commit into
mainfrom
rabbilyasar/bl-209-credential-default-gate

Conversation

@rabbilyasar

Copy link
Copy Markdown
Owner

Fixes #96

In one line: a schema whose defaultValue holds a password (one written by a pre-#94 init, or by hand) now fails to load with a message naming the field. The pre-commit check blocks it, and schema diff never prints it.

Problem (BL-209, P1)

#94 stopped init from writing credentials into the schema, but projects that already ran it may have one committed. A schema with defaultValue = "redis://:password@cache" passed scan, the pre-commit hook and check.

Design (maintainer decision on #96: option 1)

  • Load gate. CredentialDefaultError is raised at the same two full-schema load points as BL-001's secret-default gate, whatever secret says. It also covers an inherited (extends) default and a per-service override (schema_scope). The message names the field, how to fix it (remove the default, secret = true) and to rotate the password if it was committed. It never includes the value.
  • One rule. value_embeds_credential moves from the importer to schema_types, so the importer (init writes a password from a URL into the committed schema as a defaultValue (BL-208) #94) and the gate share it.
  • Backstops in the renderers (generated Python and TypeScript, .env.example, explain) via schema_types.default_must_not_render.

Found and fixed in the same change (same invariant)

  1. The pre-commit hook failed open. scan --staged skipped any schema that failed to load and passed, so a schema refused for holding a secret was committable. It now fails closed on CredentialDefaultError and SecretDefaultConflictError. An unrelated broken schema is still skipped as before, so a half-edited schema in one service doesn't block commits in another.
  2. schema diff and check --since printed both values of a changed default from history.
    • Revisions aren't load-gated: gating them would break diffing the very commit that removes a leak.
    • A default change on a secret field, or one whose default holds a credential, is now reported without values: detail: {before: null, after: null, redacted: true}.
    • A plain default change still shows its values.

Alternatives

  • A doctor/check warning without failing the load (option 2): rejected.
  • A generic URL-credential scan rule (option 3): left to Gitleaks and GitHub push protection (§14).

Security

Invariant: a secret value must never enter an output representation (an error, JSON, a diff, a generated file). The tests exercise the class.

Compatibility

  • A project whose schema already holds such a default now fails every command until it's removed. This is deliberate. The v5 migration guide must say so.
  • The JSON for a redacted default_changed adds redacted: true and nulls before/after.

Tests

  • envshield/tests/test_credential_default_gate.py (58), covering every credential form from init writes a password from a URL into the committed schema as a defaultValue (BL-208) #94's tests through:
    • loading (secret = false and absent), extends, a per-service override;
    • check (human, --json, --code-only), doctor, schema sync (writes no .env.example), explain, generate, scan --staged;
    • the renderers, and schema diff (library, and the CLI on the fixing commit).
  • Against the old code, 43 of the 58 fail.
  • Full suite: 2426 passed, 1 skipped. ruff check and ruff format --check clean.

On a real repository

The cal.com trial clone, with a pre-fix leak committed (DATABASE_URL holding a password):

  • check, doctor, schema sync, explain and scan --staged all exit 1 naming DATABASE_URL, and none prints the password.
  • The commit that removes it diffs with exit 0, without the old value, and check --since HEAD --code-only passes.

Product / market impact

Table stakes: "secret values never leave the place they belong" (§1). It closes the gap #94 left for existing projects.

🤖 Generated with Claude Code

https://claude.ai/code/session_01376EvgPvifpHL8LP5BYoPc

@rabbilyasar
rabbilyasar merged commit a046624 into main Oct 28, 2026
4 checks passed
@rabbilyasar
rabbilyasar deleted the rabbilyasar/bl-209-credential-default-gate branch October 28, 2026 19:38
…rint one (BL-209)

Loading a schema now raises CredentialDefaultError when a defaultValue
holds a credential (a password in a URL or connection string), whatever
`secret` says: the same gate as BL-001's, at the same load points, also
for an inherited default and a per-service override. The error names the
field and the fix, never the value. value_embeds_credential moves to
schema_types so the importer and the gate share one rule.

Two leaks of the same invariant, fixed here:
- `scan --staged` (the pre-commit hook) skipped a schema that failed to
  load and passed; it now fails closed when the schema holds a secret,
  and still skips an unrelated broken schema.
- `schema diff` and `check --since` printed both values of a changed
  default from history; a secret's or a credential's default change is
  now reported without values.

Fixes #96. Marks #94 (BL-208) merged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A password already in a committed schema or config file passes scan, the pre-commit hook and check (BL-209)

1 participant