[digester3] Getting ClassCastException and NoSuchMethodError in odd situations

Christopher Schultz <[email protected]> Fri, 8 Nov 2024 15:15:03 -0500
Newsgroups gmane.comp.jakarta.commons.user
Message-ID <[email protected]>
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