All,

I have a home-built SAML parser based upon commons-digester3. It's very basic and just tries to extract some minimal information from the document.

The code is deployed into a web application but there isn't anything really servlet-y around the code. It looks like this:

        Digester digester = new Digester();
        // NOTE: Don't set a rule namespace, because there are tons of
// namespaces in this document. Just ignore namespaces for now.
        digester.setNamespaceAware(true);
        digester.setValidating(true);

        digester.setLogger(digesterLogger); // for debugging this issue

        SAMLResponse samlResponse = new SAMLResponse();
        digester.push(samlResponse);

digester.addSetProperties("Response", new String[] { "Destination" }, new String[] { "Destination" });

digester.addCallMethod("Response/Issuer", "setIssuer", 1, SINGLE_STRING);
        digester.addCallParam("Response/Issuer", 0);

digester.addCallMethod("Response/Assertion/Subject/NameID", "setSubject", 1, SINGLE_STRING); digester.addCallParam("Response/Assertion/Subject/NameID", 0); // The node itself

digester.addCallMethod("Response/Assertion/Conditions", "setValidNotBefore", 1, SINGLE_STRING); digester.addCallParam("Response/Assertion/Conditions", 0, "NotBefore");

digester.addCallMethod("Response/Assertion/Conditions", "setValidNotOnOrAfter", 1, SINGLE_STRING); digester.addCallParam("Response/Assertion/Conditions", 0, "NotOnOrAfter");

digester.addCallMethod("Response/Assertion/Conditions/AudienceRestriction/Audience", "setAttribute", 2, TWO_STRINGS); digester.addObjectParam("Response/Assertion/Conditions/AudienceRestriction/Audience", 0, SSOConfig.Attributes.SYSTEM_ID); digester.addCallParam("Response/Assertion/Conditions/AudienceRestriction/Audience", 1); // The node itself

digester.addCallMethod("Response/Assertion/AttributeStatement/Attribute", "setAttribute", 3, THREE_STRINGS); digester.addRule("Response/Assertion/AttributeStatement/Attribute/AttributeValue", PUSH_RULE); // See definition digester.addCallParam("Response/Assertion/AttributeStatement/Attribute/AttributeValue", 0, true); // pop type from stack digester.addCallParam("Response/Assertion/AttributeStatement/Attribute", 1, "Name"); digester.addCallParam("Response/Assertion/AttributeStatement/Attribute/AttributeValue", 2);



digester.addCallMethod("Response/Assertion/Subject/NameID", "setAttribute", 2, TWO_STRINGS); digester.addObjectParam("Response/Assertion/Subject/NameID", 0, SSOConfig.Attributes.USERNAME); digester.addCallParam("Response/Assertion/Subject/NameID", 1); // The node itself

        try {
            digester.parse(in);
        } ...

Very rarely, I'm getting ClassCastException and NoSuchMethodError during the parsing of these documents. I added the "digesterLogger" to capture TRACE logs during processing of the message so I can dump those out in the cases where these kinds of errors occur.

When I take the document "home" and process it in e.g. a unit test, it parses without issue.

It almost looks to me like the Digester stack is being corrupted.

The samlResponse object being pushed never interacts with the Digester, other than to have the Digester call POJO-type methods on it.

The PUSH_RULE is this:

    private static class PushXSITypeAttributeRule
        extends org.apache.commons.digester3.Rule
    {
        private Logger log;

        PushXSITypeAttributeRule(Logger log) {
            this.log = log;
        }

        @Override
public void begin(String namespace, String name, org.xml.sax.Attributes attributes) { String attrType = attributes.getValue("http://www.w3.org/2001/XMLSchema-instance";, "type");
            if(null == attrType) {
                if(log.isDebugEnabled()) {
log.debug("Could not find namespaced 'type' attribute for AttributeValue; using 'string' as a fallback");
                }

                attrType = "string"; // default
            } else {
                if(attrType.startsWith("xsd:")) {
                    attrType = attrType.substring(4);
                } else if(attrType.startsWith("xs:")) {
                    attrType = attrType.substring(3);
                } else {
throw new IllegalStateException("Unrecognized attribute type: " + attrType);
                }
            }
            getDigester().push(attrType);
        }

        @Override
        public void end(String namespace, String name) {
            getDigester().pop(); // Remove String value
        }
    }
private final PushXSITypeAttributeRule PUSH_RULE = new PushXSITypeAttributeRule(logger);

I use this because (a) the "type" attribute needs to be namespaced (b) it needs to have a default value and (c) I honestly couldn't figure out how to do this in a better way.

I've seen this fail in the wild in several ways. Here's one way. SAML has a set of elements that look like this:

  <saml2:Assertion>
<saml2:Conditions NotBefore="2024-11-08T14:29:49.388Z" NotOnOrAfter="2024-11-08T14:39:49.388Z">
      <saml2:AudienceRestriction>
        <saml2:Audience>client-id</saml2:Audience>
      </saml2:AudienceRestriction>
    </saml2:Conditions>
  ...more stuff...

If you look at my setup code, I'm expecting to do the following:

1. Call SAMLResponse.setNotBefore with param Conditions.@NotBefore
2. Call SAMLResponse.setNotOnOrAfter with param Conditions.@NotOnOrAfter
3. Call SAMLResponse.setAttribute with the params SYSTEM_ID (static string) and "client-id" (from the document)

To me, it doesn't matter in what order those things happen.

When I'm seeing in the TRACE log is this:

DEBUG:   New match='Response/Assertion/Conditions'
DEBUG: Fire begin() for CallMethodRule[methodName=setValidNotBefore, paramCount=1, paramTypes={java.lang.String}]
TRACE: Pushing params
DEBUG: Fire begin() for CallParamRule[paramIndex=0, attributeName=NotBefore, from stack=false] DEBUG: Fire begin() for CallMethodRule[methodName=setValidNotOnOrAfter, paramCount=1, paramTypes={java.lang.String}]
TRACE: Pushing params
DEBUG: Fire begin() for CallParamRule[paramIndex=0, attributeName=NotOnOrAfter, from stack=false]
DEBUG:   Pushing body text ''
DEBUG:   New match='Response/Assertion/Conditions/AudienceRestriction'
DEBUG: No rules found matching 'Response/Assertion/Conditions/AudienceRestriction'.
DEBUG:   Pushing body text ''
DEBUG: New match='Response/Assertion/Conditions/AudienceRestriction/Audience' DEBUG: Fire begin() for CallMethodRule[methodName=setAttribute, paramCount=2, paramTypes={java.lang.String, java.lang.String}]
TRACE: Pushing params
DEBUG: Fire begin() for ObjectParamRule[paramIndex=0, attributeName=null, param=SYSTEM_ID] DEBUG: Fire begin() for CallParamRule[paramIndex=1, attributeName=null, from stack=false]
DEBUG:   match='Response/Assertion/Conditions/AudienceRestriction/Audience'
DEBUG:   bodyText='client-id'
DEBUG: Fire body() for CallMethodRule[methodName=setAttribute, paramCount=2, paramTypes={java.lang.String, java.lang.String}] DEBUG: Fire body() for ObjectParamRule[paramIndex=0, attributeName=null, param=SYSTEM_ID] DEBUG: Fire body() for CallParamRule[paramIndex=1, attributeName=null, from stack=false]
DEBUG:   Popping body text ''
DEBUG: Fire end() for CallParamRule[paramIndex=1, attributeName=null, from stack=false] DEBUG: Fire end() for ObjectParamRule[paramIndex=0, attributeName=null, param=SYSTEM_ID] DEBUG: Fire end() for CallMethodRule[methodName=setAttribute, paramCount=2, paramTypes={java.lang.String, java.lang.String}]
TRACE: Popping params
TRACE: [CallMethodRule]{Response/Assertion/Conditions/AudienceRestriction/Audience} parameters[0]=SYSTEM_ID TRACE: [CallMethodRule]{Response/Assertion/Conditions/AudienceRestriction/Audience} parameters[1]=client-id DEBUG: [CallMethodRule]{Response/Assertion/Conditions/AudienceRestriction/Audience} Call SAMLResponse.setAttribute(SYSTEM_ID/java.lang.String, client-id/java.lang.String)
DEBUG:   match='Response/Assertion/Conditions/AudienceRestriction'
DEBUG:   bodyText=''
DEBUG: No rules found matching 'Response/Assertion/Conditions/AudienceRestriction'.
DEBUG:   Popping body text ''
DEBUG:   match='Response/Assertion/Conditions'
DEBUG:   bodyText=''
DEBUG: Fire body() for CallMethodRule[methodName=setValidNotBefore, paramCount=1, paramTypes={java.lang.String}] DEBUG: Fire body() for CallParamRule[paramIndex=0, attributeName=NotBefore, from stack=false] DEBUG: Fire body() for CallMethodRule[methodName=setValidNotOnOrAfter, paramCount=1, paramTypes={java.lang.String}] DEBUG: Fire body() for CallParamRule[paramIndex=0, attributeName=NotOnOrAfter, from stack=false]
DEBUG:   Popping body text ''
DEBUG: Fire end() for CallParamRule[paramIndex=0, attributeName=NotOnOrAfter, from stack=false] DEBUG: Fire end() for CallMethodRule[methodName=setValidNotOnOrAfter, paramCount=1, paramTypes={java.lang.String}]
TRACE: Popping params
TRACE: [CallMethodRule]{Response/Assertion/Conditions} parameters[0]=2024-11-08T14:39:49.388Z DEBUG: [CallMethodRule]{Response/Assertion/Conditions} Call java.lang.String.setValidNotOnOrAfter(2024-11-08T14:39:49.388Z/java.lang.String)
ERROR: End event threw exception
ERROR: An error occurred while parsing XML from '(already loaded from stream)', see nested exceptions

The exception in this case is NoSuchMethodError, because the target object is a String. I'm expecting the target object on top of the stack to be a SAMLResponse object.

It looks like after SAMLResponse.setAttribute("SYSTEM_ID", "client-id") is called successfully, either some string value is left over on the stack or something is pushed on there that's unexpected.

As I said, it works every time in a unit test.

I'm not sure what the best way is to print the value of the object on the top of the stack which is a String and not a SAMLResponse object. That way, I might know what part of this code is getting sloppy / or otherwise corrupting the stack.

As far as I know, there are no other threads interacting with this Digester instance: it's scoped only for this single method.

I have another example with is very similar, except that the setAttribute() method is being called with the SAMLResponse object being the first parameter instead of an expected String value.

Any suggestions for how to debug this any further? Or something about the code above that looks suspicious or likely to fail in some weird edge-case?

I cannot make it fail on demand which, of course, is frustrating. But it happens often enough that I *can* add more instrumentation to it to help narrow-down what's going on.

Thanks,
-chris


---------------------------------------------------------------------
To unsubscribe, e-mail: user-unsubscr...@commons.apache.org
For additional commands, e-mail: user-h...@commons.apache.org

Reply via email to