Product documentation
CI security guards
The build-time scripts/check-*.sh family that makes each security invariant in this dossier un-regressable — a regression fails the PR before it can merge.
CI security guards
The invariants described elsewhere in this dossier are not documentation-only promises — each
is backed by a build-time guard in scripts/check-*.sh, wired into .github/workflows/ci.yml
and mirrored by a scripts/local-ci/* wrapper for pre-push runs. A change that re-opens one of
these holes fails the PR before it can merge. For a security reviewer this is the important
meta-property: the controls are enforced by construction, so they cannot silently rot as the
codebase evolves.
Most guards were paid for by a specific pentest finding or CI incident (referenced by their
SSO- ticket) and carry a narrow, documented suppression marker for the rare legitimate
exception — a bare marker with no rationale still fails, so an exception is always explained in
the diff.
Token, crypto & protocol integrity
| Guard | Prevents | Protects |
|---|---|---|
check-jwt-decoder-algorithms.sh | A NimbusJwtDecoder built without a JWS algorithm allow-list accepting whatever the JWKS advertises (which under some Nimbus configs admits none/HMAC). Every decoder must pin ES256 (or a justified multi-alg set). | Tenant isolation, Signing & crypto |
check-no-default-typing.sh | Jackson polymorphic default-typing (activateDefaultTyping, …), the API surface behind the long deserialization-gadget CVE chain. Banned outside a documented, cryptographically-gated exemption. | Deserialization safety |
check-spring-as-principal-factor.sh | A Spring AS authorization/token-flow principal missing a FactorGrantedAuthority, which breaks ID-token issuance (and the auth-code flow) at runtime. | Protocol integrity |
Web, session & error hygiene
| Guard | Prevents | Protects |
|---|---|---|
check-no-raw-cookie-construction.sh | A raw Cookie(...) that omits the hub's four session-cookie attributes (HttpOnly, Secure, SameSite=Lax, Path=/). All cookies must route through securedHubCookie; the clear-cookie path must carry the same attributes. | Session security |
check-no-custom-error-shape.sh | A new {error, message} error envelope outside the RFC 6749 allow-list — the shape that leaked tenant ids in the SSO-834 pentest. Everything customer-plane is RFC 9457 problem-details. | Token security |
check-no-raw-mdc-put.sh | A raw MDC.put(...), which can inject CRLF into logs (log forging). Writes must go through MdcSafe (allow-list regex + length cap). | Log integrity |
Host-header URL poisoning
Three guards fail the build when production code reads the HTTP Host header
(request.serverName / getServerName() / request.requestURL) and builds one of these
user-reachable URLs from it — the attacker-controllable Host-poisoning bug class (compare
CVE-2017-6056). Each requires the URL to come from a configured, deploy-time property instead:
check-no-host-header-in-email-url-builder.sh— password-reset and other email URLs (SSO-1134).check-no-host-header-in-oauth-redirect-uri-builder.sh— federation-member OAuthredirect_urivalues (SSO-832).check-no-host-header-in-logout-redirect.sh— post-logout redirect URIs (SSO-833).
Tenant isolation
check-no-tenant-in-customer-plane-path.sh(SSO-1937) — scans every product-api controller (not just the diff) and fails if any Spring mapping literal carries a path-borne tenant id. The tenant is always the caller'stntclaim, never the URL — see tenant isolation.
Availability & reactive safety
check-block-without-duration.sh(SSO-771) — every reactive→blocking.block()must carry an explicitDuration. An unbounded.block()pins a worker thread until the downstream completes; under load that exhausts the pool and turns a transient downstream blip into a multi-minute outage. Genuinely-safe calls carry an inline// safe:justification.
Kubernetes & infrastructure hardening
| Guard | Prevents | Protects |
|---|---|---|
check-pod-security-context.sh | A pod spec missing the shared runAsNonRoot: true + seccomp RuntimeDefault security context — the container-escape mitigation (SSO-843). | Runtime isolation |
check-deployment-sa-token.sh | A new Deployment without automountServiceAccountToken: false, which would mount a Kubernetes API token into a pod that has no business calling the API (CVE-2025-55182 follow-up). | Blast-radius containment |
check-audit-internal-k8s-enabled.sh | The internal audit endpoint drifting back to permitAll(); it must require Kubernetes ServiceAccount-JWT auth in staging/prod (SSO-1337). | Audit integrity |
check-no-literal-vault-root.sh | A literal secrets-backend root token baked into any shipped artefact (SSO-798). | Secret hygiene |
check-no-literal-compose-secrets.sh | Literal secrets embedded in compose files. | Secret hygiene |
check-ingress-tls-annotations.sh | Ingress resources missing required TLS annotations. | Transport security |
Data safety
These guards protect the durability and integrity of user data — a security-adjacent property of a system of record:
check-migration-immutability.sh— no in-place edit/rename/delete of an already-applied Flyway migration (checksum integrity).check-no-drop-database.sh— bansDROP DATABASE/TRUNCATE TABLE/DROP SCHEMAoutside the allow-list; recovery is forward-patch-only, never destructive, so real user data is never silently lost.check-no-unbounded-varchar.sh— variable-length content columns must beTEXT, notVARCHAR(n); an overflow otherwise surfaces as an opaque500that breaks a live flow.
How to read a guard as a reviewer
Each guard's header comment states the ticket, the bug it prevents, the exact pattern it
matches, and its suppression marker. When auditing a change, the fastest way to confirm an
invariant still holds is to check the guard is present in .github/workflows/ci.yml and green —
the guard, not the prose, is the enforced contract. This dossier's other pages name the specific
guard that backs each control.
Source: scripts/check-*.sh; .github/workflows/ci.yml; CLAUDE.md → "Known pitfalls" (the
rationale for most guards).