[
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]