[
https://issues.apache.org/jira/browse/RAMPART-460?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18126729#comment-18126729
]
Robert Lazarski commented on RAMPART-460:
-----------------------------------------
Added in eb824bd7: set <ramp:usernameTokenValidatorClass> in RampartConfig to a
class implementing org.apache.wss4j.dom.validate.Validator. The simplest
route is to
extend UsernameTokenValidator and override verifyPlaintextPassword, which
receives
both the username and the received password, so it can pass them to your
external
provider and throw on rejection. Without the setting, the default
RampartUsernameTokenValidator is used as before. If the configured class
can't be
loaded or isn't a Validator, the message is rejected rather than falling back
to the
default.
> 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]