A way to handle malicious XML with Expat / was Re: Handling malicious XML with Expat - what options do I have?

Sebastian Pipping <[email protected]> Fri, 12 Sep 2008 22:29:33 +0200
Newsgroups gmane.text.xml.expat.general
Message-ID <[email protected]>
I've been playing around with the Expat API and feeding a parser
instance with "a billion laughs" [1].  The approach I am taking
is counting entity value length manually inside of a custom
XML_EntityDeclHandler.  Demo code is attached, here is an excerpt
of its output:

  BEGIN handleEntityDeclaration
    laugh0 := "ha"
    Length is 2
  END

  BEGIN handleEntityDeclaration
    laugh1 := "&laugh0;&laugh0;"
    Length is 4
  END

  ..

  BEGIN handleEntityDeclaration
    laugh16 := "&laugh15;&laugh15;"
    Length is 131072
  END

  Content consided malicious XML, aborting

As Python also exposes Expat's XML_EntityDeclHandler function
I expect this approach to work for Python as well.

Comments welcome.



Sebastian


[1] http://www.cogsci.ed.ac.uk/~richard/billion-laughs.xml

_______________________________________________
Expat-discuss mailing list
[email protected]
http://mail.libexpat.org/mailman/listinfo/expat-discuss
demo_1_0.cpp (text/plain, 6.5 KB)
/*
 * Demo of handling malicious XML with Expat (tested with Expat 2.0.1)
 * v1.0 2008-09-12
 *
 * Copyright (c) 2008 Sebastian Pipping
 *
 * == The MIT License ==
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * Sebastian Pipping <[email protected]>
 */

#include <expat.h>
#include <cstdio>
#include <cstring>
#include <map>

const int MAX_BYTES_PER_ENTITY_VALUE=100000;

struct XmlCharStringCompare {
	bool operator()(XML_Char const * s1,
			XML_Char const * s2) const {
		return strcmp(s1, s2) < 0;
	}
};

typedef std::map<XML_Char const *, int, XmlCharStringCompare> MapType;
typedef std::pair<XML_Char const *, int> PairType;


MapType entityNameToValueLen;


void
handleCharacterData(void *userData, const XML_Char *s, int len) {
	::puts("BEGIN handleCharacterData");
	XML_Char * toPrint = new XML_Char[len + 1];
	::strncpy(toPrint, s, len);
	toPrint[len] = '\0';
	::printf("  \"%s\"\n", toPrint);
	delete [] toPrint;
	::puts("END\n");
}

XML_Char *
makeString(XML_Char const * first, XML_Char const * afterLast) {
	size_t const len = afterLast - first;
	XML_Char * dup = new XML_Char[len + 1];
	::strncpy(dup, first, len);
	dup[len] = '\0';
	return dup;
}

XML_Char *
nextEntityRefMalloc(XML_Char const * start,
		XML_Char const * & atAmpersand,
		XML_Char const * & afterSemiColon) {
	XML_Char const * walker = start;
	while (true) {
		switch (walker[0]) {
		case '\0':
			// No complete entity found
			atAmpersand = start;
			afterSemiColon = walker;
			return NULL;

		case '&':
			// Entity start found
			atAmpersand = walker;
			break;

		case ';':
			// Entity stop found
			if (atAmpersand != NULL) {
				afterSemiColon = walker + 1;
				return makeString(atAmpersand + 1, walker);
			}
			break;
		}
		walker++;
	}
}

int
getValueLenForEntity(XML_Char const * entityName) {
	MapType::iterator found = entityNameToValueLen.find(entityName);
	if (found != entityNameToValueLen.end()) {
		return found->second;
	} else {
		return 0;
	}
}

void
setValueLenForEntity(XML_Char const * entityName, int valueLen) {
	entityNameToValueLen.insert(PairType(entityName, valueLen));
}

void
handleEntityDeclaration(void *userData, const XML_Char *entityName,
		int is_parameter_entity, const XML_Char *value,
		int value_length, const XML_Char *base, const XML_Char *systemId,
		const XML_Char *publicId, const XML_Char *notationName) {
	::puts("BEGIN handleEntityDeclaration");
	::printf("  %s := \"%s\"\n", entityName, value);

	XML_Char const * walker = value;
	int valueLen = 0;
	while (walker[0] != '\0') {
		XML_Char const * atAmpersand = NULL;
		XML_Char const * afterSemiColon = NULL;
		XML_Char * entityRefname = nextEntityRefMalloc(walker,
				atAmpersand, afterSemiColon);
		valueLen += (atAmpersand - walker);
		if (entityRefname != NULL) {
			valueLen += getValueLenForEntity(entityRefname);
		} else {
			valueLen += (afterSemiColon - walker);
			break;
		}
		walker = afterSemiColon;
	}

	::printf("  Length is %d\n", valueLen);
	setValueLenForEntity(entityName, valueLen);

	::puts("END\n");

	if (valueLen > MAX_BYTES_PER_ENTITY_VALUE) {
		::printf("Content consided malicious XML, aborting\n", valueLen);
		XML_Parser const parser = reinterpret_cast<XML_Parser>(userData);
		::XML_StopParser(parser, XML_FALSE);
	}
}

int
main() {
	char const * const document =
#if 0
		"<!DOCTYPE d [\n"
		"\t<!ENTITY a1 \"1a1\">\n"
		"\t<!ENTITY a2 \"2&a1;2&a1;2\">\n"
		"\t<!ENTITY a4 \"4&a2;4&a2;4\">\n"
		"]>\n"
		"<t>&a4;</t>\n"
#else
	// From http://www.cogsci.ed.ac.uk/~richard/billion-laughs.xml
	"<?xml version=\"1.0\"?>\n"
	"<!DOCTYPE billion [\n"
	"<!ELEMENT billion (#PCDATA)>\n"
	"<!ENTITY laugh0 \"ha\">\n"
	"<!ENTITY laugh1 \"&laugh0;&laugh0;\">\n"
	"<!ENTITY laugh2 \"&laugh1;&laugh1;\">\n"
	"<!ENTITY laugh3 \"&laugh2;&laugh2;\">\n"
	"<!ENTITY laugh4 \"&laugh3;&laugh3;\">\n"
	"<!ENTITY laugh5 \"&laugh4;&laugh4;\">\n"
	"<!ENTITY laugh6 \"&laugh5;&laugh5;\">\n"
	"<!ENTITY laugh7 \"&laugh6;&laugh6;\">\n"
	"<!ENTITY laugh8 \"&laugh7;&laugh7;\">\n"
	"<!ENTITY laugh9 \"&laugh8;&laugh8;\">\n"
	"<!ENTITY laugh10 \"&laugh9;&laugh9;\">\n"
	"<!ENTITY laugh11 \"&laugh10;&laugh10;\">\n"
	"<!ENTITY laugh12 \"&laugh11;&laugh11;\">\n"
	"<!ENTITY laugh13 \"&laugh12;&laugh12;\">\n"
	"<!ENTITY laugh14 \"&laugh13;&laugh13;\">\n"
	"<!ENTITY laugh15 \"&laugh14;&laugh14;\">\n"
	"<!ENTITY laugh16 \"&laugh15;&laugh15;\">\n"
	"<!ENTITY laugh17 \"&laugh16;&laugh16;\">\n"
	"<!ENTITY laugh18 \"&laugh17;&laugh17;\">\n"
	"<!ENTITY laugh19 \"&laugh18;&laugh18;\">\n"
	"<!ENTITY laugh20 \"&laugh19;&laugh19;\">\n"
	"<!ENTITY laugh21 \"&laugh20;&laugh20;\">\n"
	"<!ENTITY laugh22 \"&laugh21;&laugh21;\">\n"
	"<!ENTITY laugh23 \"&laugh22;&laugh22;\">\n"
	"<!ENTITY laugh24 \"&laugh23;&laugh23;\">\n"
	"<!ENTITY laugh25 \"&laugh24;&laugh24;\">\n"
	"<!ENTITY laugh26 \"&laugh25;&laugh25;\">\n"
	"<!ENTITY laugh27 \"&laugh26;&laugh26;\">\n"
	"<!ENTITY laugh28 \"&laugh27;&laugh27;\">\n"
	"<!ENTITY laugh29 \"&laugh28;&laugh28;\">\n"
	"<!ENTITY laugh30 \"&laugh29;&laugh29;\">\n"
	"]>\n"
	"<billion>&laugh30;</billion>\n"
#endif
	;

	XML_Parser const parser = ::XML_ParserCreate(NULL);
	::XML_SetCharacterDataHandler(parser, handleCharacterData);
	::XML_SetEntityDeclHandler(parser, handleEntityDeclaration);
	::XML_UseParserAsHandlerArg(parser);

	XML_Status const res = ::XML_Parse(parser, document, strlen(document), 1);
	bool const good = (res == XML_STATUS_OK);
	if (good) {
		::puts("All good.");
	} else {
		::printf("Error (Line %d, column %d): %s\n",
				::XML_GetCurrentLineNumber(parser),
				::XML_GetCurrentColumnNumber(parser),
				::XML_ErrorString(::XML_GetErrorCode(parser)));
	}

	::XML_ParserFree(parser);
	return good ? 0 : 1;
}