Andreas Martens created RAMPART-460:
---------------------------------------

             Summary: 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


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