Skip to content

Add THREAT_MODEL.md per the Apache security threat-model rubric#1224

Open
jamesfredley wants to merge 2 commits into
apache:8.0.xfrom
jamesfredley:docs/threat-model-8.0.x
Open

Add THREAT_MODEL.md per the Apache security threat-model rubric#1224
jamesfredley wants to merge 2 commits into
apache:8.0.xfrom
jamesfredley:docs/threat-model-8.0.x

Conversation

@jamesfredley
Copy link
Copy Markdown
Contributor

@jamesfredley jamesfredley commented May 20, 2026

Introduces three top-level documents binding the 8.0.x branch:

  • THREAT_MODEL.md - prose threat model following the Apache security-team rubric, covering all eight plugins (core, acl, compat shim, ldap, cas, oauth2, rest/jwt, ui) plus the four REST token-storage backends. Sections cover scope, trust boundaries, configuration variants, per-input trust, adversaries, properties provided (P1-P15), properties disclaimed, downstream responsibilities, known misuse patterns, known non-findings, conditions that would change the model, the closed set of triage dispositions, the §14 ratification log, and a pointer to the machine-readable companion.

  • threat-model.yaml - machine-readable companion indexing components, config knobs, entry points, adversaries, claimed and disclaimed properties, false friends, known non-findings, the closed disposition set, the ratification log, and the follow-up item list. Intended for automated triage tooling.

  • SECURITY.md - disclosure-process artifact pointing reporters at the ASF Security Team (security@apache.org) and cross-referencing the threat-model sections that govern triage.

Status

DRAFT - awaiting PMC review. §14 contains a ratification log with code-grounded resolutions for all 22 open questions originally on the PMC's ratification gate; the inline *(inferred)* provenance tags tied to a resolved question have been promoted to *(maintainer)*. PMC ratification = review of the 22 resolutions. No PMC member is asked to compose answers from scratch; they confirm, correct, or strike each resolution.

§14 ratification log - summary

Wave 1 - scope and intended use

Q Topic Resolution
Q1 Caller-role split Adopt §2 as-is; anonymous-token posture is a sub-state of unauthenticated, not a separate principal
Q2 UI plugin endpoints unprotected Confirmed - zero @Secured across all 13 UI controllers; zero default Requestmap rows
Q3 alg=none acceptance Reachable misconfiguration when operator opts out of useSignedJwt (defaults true); JwtService.groovy:71-76 PlainJWT branch falls through silently. Disposition: OUT-OF-MODEL: non-default-build today; MODEL-GAP if a path is found that does not require operator opt-out. Follow-up F1 closes the gap at startup
Q4 cas.useSingleSignout disables session-fixation prevention BY-DESIGN; SpringSecurityCasGrailsPlugin.groovy:85-89 forces useSessionFixationPrevention=false with explicit source comment
Q5 security.ui.encodePassword=false default Keep false; flipping would double-hash s2-quickstart-generated apps that have a PasswordEncoderListener
Q6 bcrypt.logrounds=10 Production default (matches Spring Security upstream BCryptPasswordEncoder); test environment uses 4
Q7 spring.security.user.name bridging Intentional; {noop} prefix mirrors Spring Boot's UserDetailsServiceAutoConfiguration

Wave 2 - trust boundaries

Q Topic Resolution
Q8 IpAddressFilter proxy-blind BY-DESIGN; lines 109,130 use request.remoteAddr only; zero X-Forwarded-For references in repo
Q9 PortResolverImpl proxy-blind BY-DESIGN with one asymmetry note: SecurityRequestHolderFilter:83-113 makes the channel decision proxy-aware via X-Forwarded-Proto, but the redirect URL construction in RetryWith*EntryPoint still uses raw request.serverPort/request.serverName
Q10 OAuth2 state via java.util.Random VALID-HARDENING; OAuth2AbstractProviderService.groovy:103 uses new Random().nextInt(999_999); zero SecureRandom usage in plugin-oauth2
Q11 OAuth2 PKCE absent VALID-HARDENING; ServiceBuilder.withPkce() never invoked anywhere in plugin-oauth2
Q12 Refresh-token reuse BY-DESIGN; RestOauthController.groovy:173-181 reassigns the supplied refresh token verbatim onto the new AccessToken
Q13 JWT logout no-op BY-DESIGN; JwtTokenStorageService.removeToken throws unconditionally; RestLogoutFilter returns HTTP 404 for JWT-backend deployments
Q14 JwtService algorithm allow-list absent VALID-HARDENING; parse() dispatches on Nimbus Java type, never compares the alg header to the configured algorithm

Wave 3 - misuse / §11a curation

Q Topic Resolution
Q15 RUN_AS_* compat shim not HMAC-signed KNOWN-NON-FINDING with a nuance: the compat shim is more permissive than upstream Spring Security 5.x - the key field is declared in both RunAsManagerImpl and RunAsImplAuthenticationProvider but never read; upstream performs a keyHash equality check via hashCode() (verified at SHA 4942459 of spring-projects/spring-security). Default useRunAs:false keeps this opt-in; follow-up F8 considers porting the upstream guard
Q16 AclClass.className operator-trusted §6 trusted-input; GormAclLookupStrategy.groovy:296-299 calls Class.forName(className, true, contextClassLoader). Nuance: AclClassController ships without @Secured (Q2), so the write path is unprotected unless operator adds a Requestmap rule
Q17 GORM User/Person/Role Serializable KNOWN-NON-FINDING; plugin generates these from s2-quickstart templates; all 22 example domain classes use Serializable with serialVersionUID = 1L and no custom readObject/writeObject
Q18 ajaxSearch paramName unvalidated VALID-HARDENING; params.paramName drives the GORM criterion property name AND is concatenated into the raw HQL order by "..." string at doSearch:171 with only double-quote wrapping
Q19 §13 disposition table closed and complete Confirmed

Meta

Q Topic Resolution
Q20 Document ownership Repo-root file, maintained by PMC, revised on §12 triggers; release branches fork with own version binding
Q21 SECURITY.md coexistence SECURITY.md is the disclosure-process artifact; this file is the model; cross-references already in place
Q22 Per-plugin docs coexistence Per-plugin AsciiDoc remains end-user-facing; Appendix A back-map enumerates the citations

Code-level follow-up items identified during ratification

10 follow-ups were identified, none blocking this document's ratification. Each will become a separate issue against this repository on PMC approval.

ID Question Action Severity
F1 Q3 Reject startup when useSignedJwt:false AND useEncryptedJwt:false AND null secret AND no key provider; mirrors existing checkJwtSecret pattern High
F2 Q4 Doc note in plugin-cas/docs/src/docs/configuration.adoc linking useSingleSignout to forced useSessionFixationPrevention=false Doc-only
F3 Q5 Startup WARN in SpringSecurityUiService.initialize() when encodePassword=false and no PasswordEncoderListener detected on User class Medium
F4 Q10 Replace new Random().nextInt(999_999) with SecureRandom or UUID.randomUUID() Medium
F5 Q11 Invoke ServiceBuilder.withPkce() in OAuth2AbstractProviderService.buildScribeService Medium
F6 Q12 Doc WARNING in tokenStorage.adoc citing RFC 6749 §10.4 on refresh-token replay Doc-only
F7 Q14 Algorithm allow-list pinning at JwtService.parse() - compare jwt.header.algorithm to configured algorithm Medium
F8 Q15 Port upstream Spring Security 5.x keyHash equality check into compat shim's RunAsImplAuthenticationProvider Low
F9 Q16 Allow-list / package-prefix check on AclClass.className before Class.forName Medium
F10 Q18 Allow-list validation on AbstractS2UiDomainController.ajaxSearch paramName; closed-set check on params.order Medium

What it claims (§8 summary)

P1-P15 across the eight plugins. The most security-critical claims:

  • P1: Passwords stored as bcrypt hashes by default.
  • P2: Session fixation prevented by default - except when cas.useSingleSignout: true (the default) is active (Q4).
  • P3: Pessimistic URL coverage (rejectIfNoRule: true).
  • P10: JWT signature verified before claims trusted - defeated when the operator sets useSignedJwt:false AND useEncryptedJwt:false AND null secret AND no key provider (Q3); follow-up F1 will close this gap at startup.
  • P15: Username enumeration via authentication-exception type is suppressed by default.

What it disclaims (§9 summary)

Highlights that surface frequently in scans:

  • CSRF protection on REST/JWT endpoints (bearer-token model).
  • Anti-bot / rate limiting on /login, /register, /forgotPassword, /api/login.
  • Reset-token and registration-code expiry.
  • Stateless JWT revocation (JwtTokenStorageService.removeToken throws unconditionally; Q13).
  • OAuth2 PKCE (Q11) and secure state (current implementation uses java.util.Random; Q10).
  • X-Forwarded-For / X-Forwarded-Port awareness in IpAddressFilter and PortResolverImpl (Q8, Q9).
  • LDAP StartTLS; default ldap.context.server is plaintext ldap://.
  • Default authorization on UI plugin endpoints (Q2 - zero @Secured across all 13 controllers).
  • Mass-assignment protection in UI domain bindings.

Companion change in grails-core

Pairs with apache/grails-core#15664, which introduces the equivalent document at the framework level. References to "Grails plugin or grails profile" are aligned across both PRs.

Assisted-by: claude-code:claude-4.7-opus

Introduces three new top-level documents binding the 8.0.x branch:

- THREAT_MODEL.md: prose threat model covering all eight plugins
  (core, acl, compat shim, ldap, cas, oauth2, rest/jwt, ui) and the
  REST token-storage backends. Follows the Apache security-team rubric
  with sections for scope, trust boundaries, configuration variants,
  inputs, adversaries, properties provided and disclaimed, downstream
  responsibilities, misuse patterns, known non-findings, conditions
  that would change the model, triage dispositions, and open questions
  for the PMC.

- threat-model.yaml: machine-readable companion indexing components,
  config knobs, entry points, adversaries, claimed and disclaimed
  properties, false friends, known non-findings, and the closed
  disposition set.

- SECURITY.md: disclosure-process artifact pointing reporters at the
  ASF Security Team and cross-referencing the threat-model sections
  that govern triage.

Status is DRAFT; section 14 lists open questions for PMC ratification.

Assisted-by: claude-code:claude-4.7-opus
Copilot AI review requested due to automatic review settings May 20, 2026 17:12
@jamesfredley jamesfredley requested a review from jdaugherty May 20, 2026 17:14
@jamesfredley
Copy link
Copy Markdown
Contributor Author

https://github.com/apache/grails-spring-security/pull/1224/changes section §14 has some open questions that we will need to answer and then can regenerate those missing portions. @bkoehm @matrei @codeconsole

Copy link
Copy Markdown
Contributor

@bkoehm bkoehm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SECURITY.md text looks fine to me. I cannot comment on threat-model.yaml as I am not familiar with this.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@jamesfredley
Copy link
Copy Markdown
Contributor Author

This PR is the last step before Mythos review of grails-spring-security.

…ded evidence

Investigated all 22 PMC open questions from the §14 ratification gate using
parallel code-evidence agents across plugin-core, plugin-rest, plugin-oauth2,
plugin-cas, plugin-ldap, plugin-ui, spring-security-compat, and external
references (Nimbus JOSE+JWT canonical docs, upstream Spring Security 5.x
RunAsManagerImpl at SHA 4942459, ASF threat-model rubric).

§14 reshaped from "open questions" to "Ratification log". Every question has a
resolution paragraph with file:line citations. Provenance tags promoted from
(inferred) to (maintainer) for 18 inline claims tied to the 22 resolved
questions.

Wave 1 (scope/intended use) - resolved:
- Q1: Five caller roles in §2 are correct primitives; anonymous-token posture
  is a sub-state of unauthenticated, not a separate principal.
- Q2: UI plugin endpoints ship unprotected by design - zero @secured across
  all 13 controllers, zero default Requestmap rows, zero authz wiring.
- Q3: alg=none acceptance confirmed reachable when operator sets
  useSignedJwt:false (default true) + useEncryptedJwt:false + null secret +
  null keyProvider. JwtService.groovy:71-76 PlainJWT branch falls through.
- Q4: cas.useSingleSignout:true is default and forces
  useSessionFixationPrevention=false. SpringSecurityCasGrailsPlugin.groovy:85-89.
- Q5: security.ui.encodePassword:false default is intentional - s2-quickstart
  generates Person with PasswordEncoderListener that hashes on
  beforeInsert/beforeUpdate; flipping default would double-hash.
- Q6: bcrypt.logrounds=10 is the production default (matches Spring Security
  upstream); 4 is test-only. §10 apache#1 ≥12 hardening recommendation stands.
- Q7: spring.security.user.name bridging is intentional; the {noop} prefix
  mirrors Spring Boot's UserDetailsServiceAutoConfiguration semantics.

Wave 2 (trust boundaries) - resolved:
- Q8: IpAddressFilter uses request.remoteAddr only (line 109,130) -
  BY-DESIGN: property-disclaimed.
- Q9: PortResolverImpl uses request.serverPort only (lines 31-33) -
  BY-DESIGN: property-disclaimed. SecurityRequestHolderFilter:83-113
  asymmetry now explicitly documented in §9 false-friend.
- Q10: OAuth2 state uses java.util.Random (line 103) - VALID-HARDENING.
- Q11: PKCE absent - ServiceBuilder.withPkce() never invoked - VALID-HARDENING.
- Q12: Refresh-token reused verbatim (RestOauthController.groovy:173-181) -
  BY-DESIGN; operators mitigate via short refreshExpiration.
- Q13: JwtTokenStorageService.removeToken throws unconditionally
  (lines 124-128); RestLogoutFilter returns HTTP 404 - BY-DESIGN.
- Q14: JwtService.parse() dispatches on Nimbus Java type, no algorithm
  allow-list - VALID-HARDENING.

Wave 3 (misuse / §11a curation) - resolved:
- Q15: Compat shim's RunAsImplAuthenticationProvider is MORE permissive than
  upstream Spring Security 5.x. The key field is declared in both classes
  but never read; upstream performs a keyHash equality check via hashCode()
  (verified at SHA 4942459 of spring-projects/spring-security). The §11a
  KNOWN-NON-FINDING stands because useRunAs:false is the default, but the
  shim's permissiveness is now explicitly recorded.
- Q16: AclClass.className feeds Class.forName directly
  (GormAclLookupStrategy.groovy:296-299) - §6 trusted-input. Nuance:
  AclClassController ships without @secured (Q2), so the write path is
  unprotected unless the operator adds a rule.
- Q17: Domain class Serializable implementations are standard - all 22
  examples use serialVersionUID=1L with no custom readObject/writeObject -
  KNOWN-NON-FINDING.
- Q18: AbstractS2UiDomainController.ajaxSearch concatenates params.paramName
  into raw HQL order-by string at doSearch line 171 with only double-quote
  wrapping - VALID-HARDENING.
- Q19: §13 disposition table confirmed closed and complete.

Meta - resolved:
- Q20: Repo-root file, maintained by PMC, revised on §12 triggers; release
  branches fork with own version binding.
- Q21: SECURITY.md is disclosure-process artifact; this file is the model;
  SECURITY.md already cross-references §3-§11a.
- Q22: Per-plugin AsciiDoc remains end-user-facing; Appendix A back-map
  enumerates the citations.

Code-level follow-up items identified during ratification (F1-F10), tracked
as separate work items:

- F1 (Q3): Startup-time rejection of useSignedJwt:false + useEncryptedJwt:false
  + null secret + null keyProvider combination.
- F2 (Q4): Doc note in plugin-cas configuration.adoc linking
  useSingleSignout=true to forced useSessionFixationPrevention=false.
- F3 (Q5): Startup WARN in SpringSecurityUiService.initialize() when
  encodePassword:false and no PasswordEncoderListener detected.
- F4 (Q10): Migrate OAuth2 state from java.util.Random to SecureRandom.
- F5 (Q11): Invoke ServiceBuilder.withPkce() in buildScribeService.
- F6 (Q12): Doc WARNING citing RFC 6749 §10.4 on refresh-token replay.
- F7 (Q14): Algorithm allow-list pinning at JwtService.parse().
- F8 (Q15): Port upstream Spring Security 5.x keyHash check into compat shim.
- F9 (Q16): Allow-list / package-prefix check on AclClass.className before
  Class.forName.
- F10 (Q18): Allow-list validation on AbstractS2UiDomainController.ajaxSearch
  paramName; closed-set check on params.order.

threat-model.yaml updated with parallel ratification_log block, followup_items
list, and updated provenance counts (22 documented / 18 maintainer / 57
inferred). §1 status promoted from "DRAFT - not yet ratified" to "DRAFT -
awaiting PMC review".

Assisted-by: claude-code:claude-4.7-opus
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.

4 participants