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]