[ 
https://issues.apache.org/jira/browse/RAMPART-460?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Robert Lazarski reassigned RAMPART-460:
---------------------------------------

    Assignee: Robert Lazarski

> Cannot validate UNT Passwords against external providers anymore
> ----------------------------------------------------------------
>
>                 Key: RAMPART-460
>                 URL: https://issues.apache.org/jira/browse/RAMPART-460
>             Project: Rampart
>          Issue Type: Bug
>    Affects Versions: 2.0.1
>            Reporter: Andreas Martens
>            Assignee: Robert Lazarski
>            Priority: Major
>
> With the change made in
> {{Compare plaintext UsernameToken passwords and stop trusting bare BSTs }}{{/ 
> d0d8ff8}}
> we can no longer validate Passwords against external providers (e.g. LDAP), 
> because the new
> {verifyPlaintextPassword} implementation requires the password to be passed 
> in. 
> In the case of external providers we pass the username and password to the 
> provider and rely on an approval or disapproval from them, they cannot return 
> the password to us for validation within \{verifyPlaintextPassword}.
> My instinct was to move the validation to our Token Validator which we use 
> for SAML, Kerberos and Certs, but that doesn't solve the Phase 1 validation 
> where WSS4J calls out to validate the password.
> I think we'll need a new type of Validator for Usernames, such that the 
> default would be your RampartUsernameTokenValidator, but we could override it 
> in our stack to provide our own.
> Here's the plan as written by Bob:
> h2. Background
> Commit {{3c67f6ec}} (2013) introduced {{RampartUsernameTokenValidator}} to 
> work around a limitation of the default WSS4J {{{}UsernameTokenValidator{}}}: 
> some deployments cannot supply a plaintext password from a callback handler 
> because they only have access to a remote authentication service that accepts 
> a username/password pair and returns accept or reject, but cannot return the 
> stored password.
> At the time, the workaround was to hand the wire password back to the 
> callback and rely on that callback throwing an exception to signal rejection. 
> Commit {{d0d8ff88}} (2026) correctly fixed a security vulnerability in that 
> design: the old {{verifyPlaintextPassword}} passed the received password into 
> the callback, then compared nothing, so any password authenticated any user 
> whose callback followed the documented contract. After that fix the validator 
> correctly calls the callback with {{{}null{}}}, reads back the expected 
> password, and compares.
> However, the fix surfaces a separate architectural problem: 
> {{RampartUsernameTokenValidator}} is hardcoded in {{{}RampartMessageData{}}}:
> // RampartMessageData.java line 260 
> this.config.setValidator(WSConstants.USERNAME_TOKEN, 
> RampartUsernameTokenValidator.class);
> This line runs unconditionally on every message. There is no 
> {{RampartConfig}} field, no message-context property, and no other mechanism 
> by which a deployer can substitute a different {{Validator}} for 
> {{USERNAME_TOKEN}} — even though exactly such a mechanism already exists for 
> the policy validator ({{{}policyValidatorCbClass{}}}) and for the password 
> callback ({{{}passwordCallbackClass{}}}).
> h2. The problem in concrete terms
> The existing deployment pattern that is now broken:
>  # A container (such as IBM App Connect Enterprise) cannot return a plaintext 
> password from a callback — the security manager receives a username and 
> password and responds accept/reject, but does not expose the stored password.
>  # Such a container registered a custom callback that called the security 
> manager directly and threw on rejection.
>  # The old {{RampartUsernameTokenValidator.verifyPlaintextPassword}} happened 
> to work because it never compared anything: the callback's exception was the 
> only guard.
>  # After {{{}d0d8ff88{}}}, the callback is invoked with {{{}null{}}}, the 
> security manager receives a null password, fails, and every inbound plaintext 
> UsernameToken is rejected regardless of its actual credential.
> The deployer cannot fix this by configuring an alternative {{{}Validator{}}}, 
> because there is no configuration option for it.
> h2. What is being asked for
> Add a {{usernameTokenValidatorClass}} field to {{{}RampartConfig{}}}, 
> parallel to the existing {{policyValidatorCbClass}} and 
> {{passwordCallbackClass}} fields. When the field is set, 
> {{RampartMessageData}} should load and register it in place of the hardcoded 
> {{{}RampartUsernameTokenValidator{}}}. When the field is absent the behaviour 
> is unchanged.
> h3. Proposed changes (sketch)
> *1. {{RampartConfig.java}}* — add field, getter, setter, serialisation:
> + public final static String USERNAME_TOKEN_VALIDATOR_CLASS_LN = 
> "usernameTokenValidatorClass"; + private String usernameTokenValidatorClass; 
> + public String getUsernameTokenValidatorClass() \{ return 
> usernameTokenValidatorClass; } + public void 
> setUsernameTokenValidatorClass(String c) \{ usernameTokenValidatorClass = c; 
> } // in serialize(): + if (getUsernameTokenValidatorClass() != null) { + 
> writer.writeStartElement(NS, USERNAME_TOKEN_VALIDATOR_CLASS_LN); + 
> writer.writeCharacters(getUsernameTokenValidatorClass()); + 
> writer.writeEndElement(); + }
> *2. {{RampartConfigBuilder.java}}* — parse the new element alongside the 
> existing ones.
> *3. {{RampartMessageData.java}}* — use the configured class, falling back to 
> the current hardcoded one:
> - this.config.setValidator(WSConstants.USERNAME_TOKEN, 
> RampartUsernameTokenValidator.class); + String utValidatorClassName = null; + 
> if (policyData != null && policyData.getRampartConfig() != null) { + 
> utValidatorClassName = 
> policyData.getRampartConfig().getUsernameTokenValidatorClass(); + } + if 
> (utValidatorClassName != null) { + Class<?> utValidatorClass = 
> Loader.loadClass(classLoader, utValidatorClassName); + 
> this.config.setValidator(WSConstants.USERNAME_TOKEN, utValidatorClass); + } 
> else { + this.config.setValidator(WSConstants.USERNAME_TOKEN, 
> RampartUsernameTokenValidator.class); + }
> *4. Schema / documentation* — add the element to the Rampart policy schema 
> and the configuration reference docs, consistent with the existing optional 
> callback fields.
> h3. Policy XML usage
> <ramp:RampartConfig xmlns:ramp="http://ws.apache.org/rampart/policy";> 
> <ramp:passwordCallbackClass>com.example.MyPasswordCallback</ramp:passwordCallbackClass>
>  + <ramp:usernameTokenValidatorClass> + com.example.MyUsernameTokenValidator 
> + </ramp:usernameTokenValidatorClass> </ramp:RampartConfig>
> h2. What a custom validator can do that the callback cannot
> A {{Validator}} implementation has access to both the username and the 
> received password at the same time. That is sufficient for a container that 
> delegates to an external authentication service (LDAP, a custom security 
> manager, PAM, etc.) which accepts a credential pair and returns 
> accept/reject. Such a service does not need to expose stored passwords, and 
> there is no way to make it fit the WSS4J callback contract — which requires 
> the callback to _supply_ the password, not verify it.
> The validator approach also keeps all UsernameToken validation logic in one 
> class, rather than splitting it across a callback (which does the real check) 
> and a validator (which does a redundant compare or, as before, nothing).
> h2. Alternatives considered
>  * *Extend the callback to support a verifying variant.* A second interface 
> (e.g. {{{}PasswordVerifyingCallbackHandler{}}}) would let the validator skip 
> the compare when the callback signals that it has already verified. This 
> avoids changes to {{{}RampartConfig{}}}, but it is a less standard extension 
> point and puts security logic in two places at once.
>  * *Make {{RampartUsernameTokenValidator}} handle the "verify only" case.* 
> The validator could check whether the callback returns a non-null password 
> (compare mode) or null (verify mode, where throwing = reject, returning = 
> accept). The {{d0d8ff88}} commit already explicitly rejected this: a callback 
> that neither sets a password nor throws has authenticated nobody. The 
> ambiguity is the root cause of the original vulnerability.
>  * *No change; force all callers to use the callback contract.* This is 
> architecturally cleanest but not possible for every authentication backend. 
> The existing {{policyValidatorCbClass}} and {{passwordCallbackClass}} fields 
> show that Rampart already accepts that different deployers have different 
> needs.
> h2. Backwards compatibility
> When the new field is absent (the default), {{RampartUsernameTokenValidator}} 
> is registered exactly as today. No existing configuration is affected. The 
> behaviour of {{RampartUsernameTokenValidator}} itself is unchanged.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to