/**
* WS-Attacker - A Modular Web Services Penetration Testing Framework Copyright
* (C) 2013 Christian Mainka
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package wsattacker.library.schemaanalyzer;
import java.util.*;
import javax.xml.namespace.QName;
import javax.xml.xpath.XPathExpressionException;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import wsattacker.library.xmlutilities.dom.DomUtilities;
import static wsattacker.library.xmlutilities.namespace.NamespaceConstants.URI_NS_SCHEMA;
public class SchemaAnalyzerImpl
implements SchemaAnalyzer
{
private final static Logger LOG = Logger.getLogger( SchemaAnalyzerImpl.class );
Document analyzingDocument, expandedAnalyzingDocument;
Map<String, Document> schemaMap;
List<QName> filterList;
public SchemaAnalyzerImpl()
{
schemaMap = new HashMap<String, Document>();
analyzingDocument = null;
expandedAnalyzingDocument = null;
filterList = new ArrayList<QName>();
}
public Document getExpandedAnalyzingDocument()
{
return expandedAnalyzingDocument;
}
public Document getAnalyzingDocument()
{
return analyzingDocument;
}
public List<QName> getFilterList()
{
return filterList;
}
/*
* (non-Javadoc)
* @see wsattacker.plugin.signatureWrapping.schema.SchemaAnalyserInterface# setFilterList(java.util.List)
*/
@Override
public void setFilterList( List<QName> filterList )
{
this.filterList = filterList;
}
/*
* (non-Javadoc)
* @see wsattacker.plugin.signatureWrapping.schema.SchemaAnalyserInterface# appendSchema(org.w3c.dom.Document)
*/
@Override
public void appendSchema( Document newSchema )
{
Element newSchemaRoot = newSchema.getDocumentElement();
// Only Append if Document is a Schema Document
if ( newSchema != null && URI_NS_SCHEMA.equals( newSchemaRoot.getNamespaceURI() )
&& "schema".equals( newSchemaRoot.getLocalName() ) )
{
String targetNamespace = newSchema.getDocumentElement().getAttribute( "targetNamespace" );
schemaMap.put( targetNamespace, newSchema );
}
}
public void clearSchemas()
{
schemaMap.clear();
}
public boolean isInCurrentAnalysis( Node n )
{
boolean result =
( analyzingDocument != null && ( n.getOwnerDocument().getDocumentElement().isEqualNode( analyzingDocument.getDocumentElement() ) ) );
LOG.trace( String.format( "isInCurrent: %b", result ) );
return result;
}
/*
* (non-Javadoc)
* @see wsattacker.plugin.signatureWrapping.schema.SchemaAnalyserInterface# findExpansionPoint(org.w3c.dom.Element)
*/
@Override
public Set<AnyElementProperties> findExpansionPoint( Element fromHere )
{
if ( !isInCurrentAnalysis( fromHere ) )
{
LOG.trace( "New Document to analyze!" );
// We will clone the Document of Node fromHere and add all possible
// expansionpoints
expandedAnalyzingDocument =
DomUtilities.createNewDomFromNode( fromHere.getOwnerDocument().getDocumentElement() );
analyzingDocument = fromHere.getOwnerDocument();
}
Document expandedDoc = expandedAnalyzingDocument; // get current
// analyzed document
Element start = DomUtilities.findCorrespondingElement( expandedDoc, fromHere ); // corresponding "fromHere"
// return a Map of <Node,Properties>
Set<AnyElementProperties> result = new TreeSet<AnyElementProperties>();
findExpansionPoint( result, start );
return result;
}
private void findExpansionPoint( Set<AnyElementProperties> result, Element start )
{
LOG.trace( "Find expansion point of Element '" + start.getNodeName() + "'" );
// Shall the Element be filtered?
if ( filterList.contains( new QName( start.getNamespaceURI(), start.getLocalName() ) ) )
{
LOG.trace( "\tFound in filterList -> Abort" );
return;
}
// Find allowed child elements of start element
List<Element> possibleChildElementList = findPossibleChildElements( start );
boolean hasAny = false;
for ( Element possibleChild : possibleChildElementList )
{
if ( "any".equals( possibleChild.getLocalName() ) )
{
if ( !hasAny )
{
hasAny = true; // add only one any child element
result.add( new AnyElementPropertiesImpl( possibleChild, start ) );
LOG.trace( "\t-> xs:any <- allowed!" );
}
}
else
{
String localName = possibleChild.getAttribute( "name" );
String[] prefixNamespacePair = getTargetNamespace( possibleChild );
LOG.trace( "\tAllowed Child: '" + prefixNamespacePair[1] + ":" + localName + "'" );
// Check if Element exists
if ( DomUtilities.findChildren( start, localName, prefixNamespacePair[1], false ).isEmpty() )
{
// Check if an identical ancestor element exists:
if ( elementHasSpecificAncestor( start, localName, prefixNamespacePair[1] ) )
{
LOG.trace( "\t\tAncestor with same name already exists -> *NOT* Created" );
}
else
{
createChildElementForElement( start, localName, prefixNamespacePair[1], prefixNamespacePair[0] );
LOG.trace( "\t\tDoes not exist -> Created" );
}
}
}
}
// Recursive with all child elements
NodeList theChildren = start.getChildNodes();
for ( int i = 0; i < theChildren.getLength(); ++i )
{
if ( theChildren.item( i ).getNodeType() == Node.ELEMENT_NODE )
{
findExpansionPoint( result, (Element) theChildren.item( i ) );
}
}
}
/**
* Checks theElement has an ancestor element with given ancestorLocalName and ancestorNamespaceURI.
*
* @param theElement
* @param ancestorLocalName
* @param ancestorNamespaceURI
* @return
*/
public boolean elementHasSpecificAncestor( Element theElement, String ancestorLocalName, String ancestorNamespaceURI )
{
boolean ret = false;
Node up = theElement.getParentNode();
while ( up != null && up.getNodeType() == Node.ELEMENT_NODE )
{
if ( up.getNamespaceURI().equals( ancestorNamespaceURI ) && up.getLocalName().equals( ancestorLocalName ) )
{
ret = true;
break;
}
up = up.getParentNode();
}
return ret;
}
public void createChildElementForElement( Element theElement, String localName, String namespaceURI,
String fallbackPrefix )
{
// search if there is already a prefix binded to the namespaceURI
String prefix = DomUtilities.getPrefix( theElement, namespaceURI );
if ( prefix == null )
{
// prefix not in elements scope.
// use prefix from schema
prefix = fallbackPrefix;
if ( prefix == null )
{
// if there is now prefix defined in the schema
// use no prefix.
prefix = "";
}
}
else if ( !prefix.isEmpty() )
{
// if we have a prefix, the QName must be
// written as prefix:localName
prefix = prefix + ':';
}
LOG.info( String.format( "\t-> Creating element %s:%s%s", namespaceURI, prefix, localName ) );
theElement.appendChild( theElement.getOwnerDocument().createElementNS( theElement.getNamespaceURI(),
prefix + localName ) );
}
public List<Element> findPossibleChildElements( Element element )
{
return findPossibleChildElements( element.getNamespaceURI(), element.getLocalName() );
}
public List<Element> findPossibleChildElements( String namespaceURI, String localName )
{
Element elementSchema = findElementInSchema( namespaceURI, localName );
if ( elementSchema == null )
{
LOG.info( String.format( "Could not find any child elements for element '%s:%s'", namespaceURI, localName ) );
return new ArrayList<Element>();
}
Element complexType = findComplexTypeForElement( elementSchema );
if ( complexType == null )
{
LOG.info( String.format( "Element '%s:%s' seems to not to have a complex declaration.", namespaceURI,
localName ) );
return new ArrayList<Element>();
}
List<Element> refferingElementList = DomUtilities.findChildren( complexType, "element", URI_NS_SCHEMA, true );
List<Element> possibleChildElementList = DomUtilities.findChildren( complexType, "any", URI_NS_SCHEMA, true );
for ( Element referringElement : refferingElementList )
{
Element schemaElement = dereferenceElement( referringElement );
if ( schemaElement != null )
{
possibleChildElementList.add( schemaElement );
}
}
return possibleChildElementList;
}
public Element findComplexTypeInSchema( String namespaceURI, String elementTypeLocalName )
{
return findXInSchema( "complexType", namespaceURI, elementTypeLocalName );
}
public Element findElementInSchema( String namespaceURI, String localName )
{
return findXInSchema( "element", namespaceURI, localName );
}
private Element findXInSchema( String x, String namespaceURI, String localName )
{
Document theSchema = schemaMap.get( namespaceURI );
if ( theSchema == null )
{
return null;
}
String xpath;
xpath =
"//*[local-name()='" + x + "' and namespace-uri()='" + URI_NS_SCHEMA + "' and @name='" + localName + "']";
List<Element> complexTypeList;
try
{
complexTypeList = (List<Element>) DomUtilities.evaluateXPath( theSchema, xpath );
}
catch ( XPathExpressionException e )
{
throw new IllegalArgumentException( "Invalid XPath. This should never happen.", e );
}
if ( complexTypeList.size() == 0 )
{
// Nothing found, abort
return null;
}
else if ( complexTypeList.size() > 1 )
{
LOG.warn( String.format( "More than one possible schema definition found for %s:%s", namespaceURI,
localName ) );
}
return complexTypeList.get( 0 );
}
public Element dereferenceElement( Element referringElement )
{
Element elementSchema = null;
String ref = referringElement.getAttribute( "ref" );
int colonPosition = ref.indexOf( ':' );
if ( colonPosition > 0 )
{
// extract element's localname
String elementTypeLocalName = ref.substring( colonPosition + 1 );
// extract element's prefix
String elementPrefix = ref.substring( 0, colonPosition );
// find element's namespaceURI
String elementNamespaceURI = DomUtilities.getNamespaceURI( referringElement, elementPrefix );
// find the complexType element definition
elementSchema = findElementInSchema( elementNamespaceURI, elementTypeLocalName );
}
return elementSchema;
}
public Element findComplexTypeForElement( Element elementSchema )
{
Element compexTypeSchema = null;
String type = elementSchema.getAttribute( "type" );
if ( type.isEmpty() )
{
// no type reference found
// maybe subelement is complexType?
List<Element> complexChildElementList =
DomUtilities.findChildren( elementSchema, "complexType", URI_NS_SCHEMA, true );
if ( !complexChildElementList.isEmpty() )
{
compexTypeSchema = complexChildElementList.get( 0 );
}
}
else
{
int colonPosition = type.indexOf( ':' );
if ( colonPosition > 0 )
{
// extract element's localname
String elementTypeLocalName = type.substring( colonPosition + 1 );
// extract element's prefix
String elementPrefix = type.substring( 0, colonPosition );
// find element's namespaceURI
String elementNamespaceURI = DomUtilities.getNamespaceURI( elementSchema, elementPrefix );
// find the complexType element definition
compexTypeSchema = findComplexTypeInSchema( elementNamespaceURI, elementTypeLocalName );
}
}
return compexTypeSchema;
}
/**
* Returns a StringPair of the targetNamespace of an Schema-Element.
*
* @param x Schema Element
* @return String[2] with {prefix,targetNS}
*/
public String[] getTargetNamespace( Element x )
{
Node parent = x;
do
{
parent = parent.getParentNode();
if ( parent != null && "schema".equals( parent.getLocalName() )
&& URI_NS_SCHEMA.equals( parent.getNamespaceURI() ) )
{
break;
}
}
while ( parent != null );
Element p;
if ( parent instanceof Element )
{
p = (Element) parent;
}
else
{
return new String[] { "", "" };
}
String targetNS = p.getAttribute( "targetNamespace" );
String prefix = "";
if ( targetNS.isEmpty() )
{
return new String[] { prefix, targetNS };
}
NamedNodeMap attributes = parent.getAttributes();
for ( int i = 0; i < attributes.getLength(); ++i )
{
Node attribute = attributes.item( i );
if ( attribute.getPrefix() != null && attribute.getPrefix().equals( "xmlns" )
&& attribute.getTextContent().equals( targetNS ) )
{
prefix = attribute.getLocalName();
break;
}
}
return new String[] { prefix, targetNS };
}
}