Re: A way to handle malicious XML with Expat / was Re: Handling malicious XML with Expat - what options do I have?
Sebastian Pipping <[email protected]> Sat, 13 Sep 2008 05:08:23 +0200
| Newsgroups | gmane.text.xml.expat.general |
|---|---|
| Message-ID | <[email protected]> |
Talking to a friend of mine gave some new ideas. Monitoring the final size of an entity alone is not enough: - The entity could evaluate to zero length and still take "forever" to compute (== billion laughs with "" instead of "ha") - Part of the content multiplication could be moved to the document's body and therefore be hidden from monitoring So I have added two more checks: - A limit on the total lookups to form the value of an entity - A limit on the ratio between input and output The three constants in the code to play with are MAX_BYTES_PER_ENTITY_VALUE = 100000 MAX_LOOKUPS_PER_ENTITY_VALUE = 30 MAX_INPUT_FACTOR = 20 I'd be interested to know if these values still work for people working with very large documents. Sebastian _______________________________________________ Expat-discuss mailing list [email protected] http://mail.libexpat.org/mailman/listinfo/expat-discuss
demo_2_0.cpp
(text/plain, 9.7 KB)
/* * Demo of handling malicious XML with Expat (tested with Expat 2.0.1) * v2.0 2008-09-13 * * 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> // Config int const MAX_BYTES_PER_ENTITY_VALUE = 100000; int const MAX_LOOKUPS_PER_ENTITY_VALUE = 30; int const MAX_OUTPUT_INPUT_RATIO = 20; struct XmlCharStringCompare { bool operator()(XML_Char const * s1, XML_Char const * s2) const { return strcmp(s1, s2) < 0; } }; struct EntityInfo { int valueLen; int maxLookups; EntityInfo(int valueLen, int maxLookups) : valueLen(valueLen), maxLookups(maxLookups) { } }; typedef std::map<XML_Char const *, EntityInfo, XmlCharStringCompare> MapType; typedef std::pair<XML_Char const *, EntityInfo> PairType; // Global vars XML_Index totalCharDataBytesServed = 0; MapType entityNameToValueLen; XML_Char * dupString(XML_Char const * source) { size_t const len = ::strlen(source); XML_Char * const dup = new XML_Char[len + 1]; ::strcpy(dup, source); return dup; } void initMap() { // Register default entities EntityInfo info(1, 0); entityNameToValueLen.insert(PairType(dupString("amp"), info)); entityNameToValueLen.insert(PairType(dupString("lt"), info)); entityNameToValueLen.insert(PairType(dupString("gt"), info)); entityNameToValueLen.insert(PairType(dupString("apos"), info)); entityNameToValueLen.insert(PairType(dupString("quot"), info)); } XML_Parser getParser(void * userData) { return reinterpret_cast<XML_Parser>(userData); } void panic(void * userData, XML_Char const * diagonis) { ::puts("\n PANIC:"); ::printf(" %s\n", diagonis); ::puts(" -> Content considered malicious XML"); ::puts(" -> Aborting"); ::XML_StopParser(getParser(userData), XML_FALSE); } void handleCharacterData(void *userData, const XML_Char *s, int len) { ::puts("BEGIN handleCharacterData"); totalCharDataBytesServed += (len * sizeof(XML_Char)); XML_Index const byteIndex = XML_GetCurrentByteIndex(getParser(userData)); XML_Char * toPrint = new XML_Char[len + 1]; ::strncpy(toPrint, s, len); toPrint[len] = '\0'; ::printf(" \"%s\"\n", toPrint); ::printf(" Byte index: %u\n", byteIndex); ::printf(" Char data bytes served: %u\n", totalCharDataBytesServed); delete [] toPrint; #if 1 // Prevent if (totalCharDataBytesServed > MAX_OUTPUT_INPUT_RATIO * byteIndex) { panic(userData, "Output/input ratio too large"); } #endif ::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++; } } EntityInfo getEntityInfo(XML_Char const * entityName) { static EntityInfo errorInfo(0, 0); MapType::iterator found = entityNameToValueLen.find(entityName); if (found != entityNameToValueLen.end()) { return found->second; } else { return errorInfo; } } void setEntityInfo(XML_Char const * name, EntityInfo const & info) { entityNameToValueLen.insert(PairType(name, info)); } 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; int maxLookups = 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) { EntityInfo const info = getEntityInfo(entityRefname); valueLen += info.valueLen; maxLookups += info.maxLookups + 1; } else { valueLen += (afterSemiColon - walker); break; } walker = afterSemiColon; } int const bytesNeeded = valueLen * sizeof(XML_Char); ::printf(" Length in bytes: %d\n", bytesNeeded); ::printf(" Maximum lookups: %d\n", maxLookups); EntityInfo const info(valueLen, maxLookups); setEntityInfo(entityName, info); #if 1 // Prevent if (bytesNeeded > MAX_BYTES_PER_ENTITY_VALUE) { panic(userData, "Entity takes too much space"); } else if (maxLookups > MAX_LOOKUPS_PER_ENTITY_VALUE) { panic(userData, "Entity requires too many lookups"); } #endif ::puts("END\n"); } int main() { initMap(); 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" # if 0 "<!ENTITY laugh0 \"ha\">\n" # else # if 0 "<!ENTITY laugh0 \"\">\n" # else "<!ENTITY laugh0 \"" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "hahahahahahahaha" "\">\n" # endif # endif "<!ENTITY laugh1 \"&laugh0;&laugh0;\">\n" "<!ENTITY laugh2 \"&laugh1;&laugh1;\">\n" # if 0 "<!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" # else "]>\n" "<foo>\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;&laugh2;\n" "</foo>" # endif #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; }