libxslt security framework bypass
libxslt security checks when accessing external resources are implemented using the xsltCheckRead
and xsltCheckWrite
functions. The function returns 1 if the access is allowed, 0 if it is disallowed and -1 in case of error.
However, none of the caller of these functions handles the error case correctly. For example, xsltLoadDocument only fails if the return value == 0:
xsltDocumentPtr
xsltLoadDocument(...) {
...
res = xsltCheckRead(ctxt->sec, ctxt, URI);
if (res == 0) {
xsltTransformError(ctxt, NULL, NULL,
"xsltLoadDocument: read rights for %s denied\n",
URI);
return(NULL);
}
Other callers behave in the same way.
xsltCheckWrite
can only fail when a small sized memory allocation fails which is probably difficult to trigger. However, xsltCheckRead
can fail when the passed URI can't be parsed:
uri = xmlParseURI((const char *)URL);
if (uri == NULL) {
xsltTransformError(ctxt, NULL, NULL,
"xsltCheckRead: URL parsing failed for %s\n",
URL);
return(-1);
}
xsltDocumentFunctionLoadDocument
the only caller of xsltLoadDocument
performs the following steps on an attacker controlled URI that includes a fragment:
- Parse URI using
xmlParseURI
(this needs to succeed) - Remove fragment from the parsed URI structure
- Turn the modified URI structure into a string using
xmlSaveUri
- Pass this new (fragmentless) URI to
xsltLoadDocument
So the an attacker needs to find an input URL where xmlParseURI(input)
works but xmlParseURI(xmlSaveUri(xmlParseURI(input))
fails and the final URL can still be used by the currently
enabled xsltDocLoaderFunc
.
A simple example to exploit this behaviour is using the following input string as an url in a xslt document() call:
http://%61%3a%5c%62%40%6c%6f%63%61%6c%68%6f%73%74%3a%32%32%2f#foo
After being parsed, modified and re-serialized using xmlSaveUri
it becomes
http://a:\b@localhost:22/
which is not a valid URL according to xmlParseURI
and which triggers the error in xsltCheckRead
. If xsltDocDefaultLoader
is used it will call
into libxml’s xmlLoadExternalEntity
which happily tries to fix invalid input arguments by calling xmlCanonicPath
on the passed argument. This will escape the invalid backslash in our example URL triggering a valid GET request to localhost:22
later on.
When the default libxml io functionality is used local file access can be triggered with something like
<xsl:copy-of select="document('file://%65%74%63%2f%70%61%73%73%77%64%25%30%30%5c%23%23#foo')"/>
thanks to the hexdecode feature of xmlFileOpen
I've attached a patch that fixes this issue by making xsltCheckRead/Write return 0 in the case of an error.
Please let me know if you have any questions.
This bug is subject to a 90 day disclosure deadline. After 90 days elapse or a patch has been made broadly available (whichever is earlier), the bug report will become visible to the public.
Suggested Patch: google-fix-check.patch