Add THREAT_MODEL.md per the Apache security threat-model rubric#1224
Open
jamesfredley wants to merge 2 commits into
Open
Add THREAT_MODEL.md per the Apache security threat-model rubric#1224jamesfredley wants to merge 2 commits into
jamesfredley wants to merge 2 commits into
Conversation
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
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 |
bkoehm
reviewed
May 20, 2026
Contributor
bkoehm
left a comment
There was a problem hiding this comment.
The SECURITY.md text looks fine to me. I cannot comment on threat-model.yaml as I am not familiar with this.
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
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.
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
@Securedacross all 13 UI controllers; zero default Requestmap rowsalg=noneacceptanceuseSignedJwt(defaultstrue);JwtService.groovy:71-76PlainJWT branch falls through silently. Disposition:OUT-OF-MODEL: non-default-buildtoday;MODEL-GAPif a path is found that does not require operator opt-out. Follow-up F1 closes the gap at startupcas.useSingleSignoutdisables session-fixation preventionSpringSecurityCasGrailsPlugin.groovy:85-89forcesuseSessionFixationPrevention=falsewith explicit source commentsecurity.ui.encodePassword=falsedefaultfalse; flipping would double-hash s2-quickstart-generated apps that have aPasswordEncoderListenerbcrypt.logrounds=10BCryptPasswordEncoder); test environment uses 4spring.security.user.namebridging{noop}prefix mirrors Spring Boot'sUserDetailsServiceAutoConfigurationWave 2 - trust boundaries
IpAddressFilterproxy-blindrequest.remoteAddronly; zeroX-Forwarded-Forreferences in repoPortResolverImplproxy-blindSecurityRequestHolderFilter:83-113makes the channel decision proxy-aware viaX-Forwarded-Proto, but the redirect URL construction inRetryWith*EntryPointstill uses rawrequest.serverPort/request.serverNamejava.util.RandomOAuth2AbstractProviderService.groovy:103usesnew Random().nextInt(999_999); zeroSecureRandomusage in plugin-oauth2ServiceBuilder.withPkce()never invoked anywhere in plugin-oauth2RestOauthController.groovy:173-181reassigns the supplied refresh token verbatim onto the newAccessTokenJwtTokenStorageService.removeTokenthrows unconditionally;RestLogoutFilterreturns HTTP 404 for JWT-backend deploymentsparse()dispatches on Nimbus Java type, never compares thealgheader to the configured algorithmWave 3 - misuse / §11a curation
RUN_AS_*compat shim not HMAC-signedkeyfield is declared in bothRunAsManagerImplandRunAsImplAuthenticationProviderbut never read; upstream performs akeyHashequality check viahashCode()(verified at SHA4942459ofspring-projects/spring-security). DefaultuseRunAs:falsekeeps this opt-in; follow-up F8 considers porting the upstream guardAclClass.classNameoperator-trustedGormAclLookupStrategy.groovy:296-299callsClass.forName(className, true, contextClassLoader). Nuance:AclClassControllerships without@Secured(Q2), so the write path is unprotected unless operator adds a Requestmap ruleUser/Person/RoleSerializables2-quickstarttemplates; all 22 example domain classes useSerializablewithserialVersionUID = 1Land no customreadObject/writeObjectajaxSearchparamNameunvalidatedparams.paramNamedrives the GORM criterion property name AND is concatenated into the raw HQLorder by "..."string atdoSearch:171with only double-quote wrappingMeta
SECURITY.mdcoexistenceSECURITY.mdis the disclosure-process artifact; this file is the model; cross-references already in placeCode-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.
useSignedJwt:falseANDuseEncryptedJwt:falseAND null secret AND no key provider; mirrors existingcheckJwtSecretpatternplugin-cas/docs/src/docs/configuration.adoclinkinguseSingleSignoutto forceduseSessionFixationPrevention=falseWARNinSpringSecurityUiService.initialize()whenencodePassword=falseand noPasswordEncoderListenerdetected on User classnew Random().nextInt(999_999)withSecureRandomorUUID.randomUUID()ServiceBuilder.withPkce()inOAuth2AbstractProviderService.buildScribeServiceWARNINGintokenStorage.adocciting RFC 6749 §10.4 on refresh-token replayJwtService.parse()- comparejwt.header.algorithmto configured algorithmkeyHashequality check into compat shim'sRunAsImplAuthenticationProviderAclClass.classNamebeforeClass.forNameAbstractS2UiDomainController.ajaxSearchparamName; closed-set check onparams.orderWhat it claims (§8 summary)
P1-P15 across the eight plugins. The most security-critical claims:
cas.useSingleSignout: true(the default) is active (Q4).rejectIfNoRule: true).useSignedJwt:falseANDuseEncryptedJwt:falseAND null secret AND no key provider (Q3); follow-up F1 will close this gap at startup.What it disclaims (§9 summary)
Highlights that surface frequently in scans:
/login,/register,/forgotPassword,/api/login.JwtTokenStorageService.removeTokenthrows unconditionally; Q13).state(current implementation usesjava.util.Random; Q10).X-Forwarded-For/X-Forwarded-Portawareness inIpAddressFilterandPortResolverImpl(Q8, Q9).ldap.context.serveris plaintextldap://.@Securedacross all 13 controllers).Companion change in grails-core
Pairs with
apache/grails-core#15664, which introduces the equivalent document at the framework level. References to "Grails plugin orgrailsprofile" are aligned across both PRs.Assisted-by: claude-code:claude-4.7-opus