Skip to content

Commit bafe22c

Browse files
authored
Merge pull request #20048 from Napalys/js/xml_bomb_sinks
JS: Exclude patched libraries from `xml-bomb` sink
2 parents 0cc9ff8 + ea93b39 commit bafe22c

File tree

11 files changed

+23
-39
lines changed

11 files changed

+23
-39
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Removed `libxmljs` as an XML bomb sink. The underlying libxml2 library now includes [entity reference loop detection](https://github.com/GNOME/libxml2/blob/0c948334a8f5c66d50e9f8992e62998017dc4fc6/NEWS#L905-L908) that prevents XML bomb attacks.

javascript/ql/lib/semmle/javascript/frameworks/XmlParsers.qll

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ module XML {
4949
override JS::Expr getSourceArgument() { result = this.getArgument(0) }
5050

5151
override predicate resolvesEntities(EntityKind kind) {
52-
// internal entities are always resolved
53-
kind = InternalEntity()
54-
or
52+
not kind = InternalEntity() and
5553
// other entities are only resolved if the configuration option `noent` is set to `true`
5654
exists(JS::Expr noent |
5755
this.hasOptionArgument(1, "noent", noent) and
@@ -126,8 +124,9 @@ module XML {
126124
override JS::Expr getSourceArgument() { result = this.getArgument(0) }
127125

128126
override predicate resolvesEntities(EntityKind kind) {
129-
// entities are resolved by default
130-
any()
127+
// SAX parsers in libxmljs also inherit libxml2's protection against XML bombs
128+
kind = ExternalEntity(_) or
129+
kind = ParameterEntity(true)
131130
}
132131

133132
override DataFlow::Node getAResult() {
@@ -149,8 +148,9 @@ module XML {
149148
override JS::Expr getSourceArgument() { result = this.getArgument(0) }
150149

151150
override predicate resolvesEntities(EntityKind kind) {
152-
// entities are resolved by default
153-
any()
151+
// SAX push parsers in libxmljs also inherit libxml2's protection against XML bombs
152+
kind = ExternalEntity(_) or
153+
kind = ParameterEntity(true)
154154
}
155155

156156
override DataFlow::Node getAResult() {

javascript/ql/test/query-tests/Security/CWE-776/XmlBomb.expected

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@
55
| domparser.js:11:57:11:59 | src | domparser.js:2:13:2:36 | documen ... .search | domparser.js:11:57:11:59 | src | XML parsing depends on a $@ without guarding against uncontrolled entity expansion. | domparser.js:2:13:2:36 | documen ... .search | user-provided value |
66
| expat.js:6:16:6:36 | req.par ... e-xml") | expat.js:6:16:6:36 | req.par ... e-xml") | expat.js:6:16:6:36 | req.par ... e-xml") | XML parsing depends on a $@ without guarding against uncontrolled entity expansion. | expat.js:6:16:6:36 | req.par ... e-xml") | user-provided value |
77
| jquery.js:4:14:4:16 | src | jquery.js:2:13:2:36 | documen ... .search | jquery.js:4:14:4:16 | src | XML parsing depends on a $@ without guarding against uncontrolled entity expansion. | jquery.js:2:13:2:36 | documen ... .search | user-provided value |
8-
| libxml.js:5:21:5:41 | req.par ... e-xml") | libxml.js:5:21:5:41 | req.par ... e-xml") | libxml.js:5:21:5:41 | req.par ... e-xml") | XML parsing depends on a $@ without guarding against uncontrolled entity expansion. | libxml.js:5:21:5:41 | req.par ... e-xml") | user-provided value |
9-
| libxml.noent.js:5:21:5:41 | req.par ... e-xml") | libxml.noent.js:5:21:5:41 | req.par ... e-xml") | libxml.noent.js:5:21:5:41 | req.par ... e-xml") | XML parsing depends on a $@ without guarding against uncontrolled entity expansion. | libxml.noent.js:5:21:5:41 | req.par ... e-xml") | user-provided value |
10-
| libxml.sax.js:6:22:6:42 | req.par ... e-xml") | libxml.sax.js:6:22:6:42 | req.par ... e-xml") | libxml.sax.js:6:22:6:42 | req.par ... e-xml") | XML parsing depends on a $@ without guarding against uncontrolled entity expansion. | libxml.sax.js:6:22:6:42 | req.par ... e-xml") | user-provided value |
11-
| libxml.saxpush.js:6:15:6:35 | req.par ... e-xml") | libxml.saxpush.js:6:15:6:35 | req.par ... e-xml") | libxml.saxpush.js:6:15:6:35 | req.par ... e-xml") | XML parsing depends on a $@ without guarding against uncontrolled entity expansion. | libxml.saxpush.js:6:15:6:35 | req.par ... e-xml") | user-provided value |
128
edges
139
| closure.js:2:7:2:36 | src | closure.js:3:24:3:26 | src | provenance | |
1410
| closure.js:2:13:2:36 | documen ... .search | closure.js:2:7:2:36 | src | provenance | |
@@ -31,8 +27,4 @@ nodes
3127
| jquery.js:2:7:2:36 | src | semmle.label | src |
3228
| jquery.js:2:13:2:36 | documen ... .search | semmle.label | documen ... .search |
3329
| jquery.js:4:14:4:16 | src | semmle.label | src |
34-
| libxml.js:5:21:5:41 | req.par ... e-xml") | semmle.label | req.par ... e-xml") |
35-
| libxml.noent.js:5:21:5:41 | req.par ... e-xml") | semmle.label | req.par ... e-xml") |
36-
| libxml.sax.js:6:22:6:42 | req.par ... e-xml") | semmle.label | req.par ... e-xml") |
37-
| libxml.saxpush.js:6:15:6:35 | req.par ... e-xml") | semmle.label | req.par ... e-xml") |
3830
subpaths

javascript/ql/test/query-tests/Security/CWE-776/libxml.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ const express = require('express');
22
const libxmljs = require('libxmljs');
33

44
express().get('/some/path', function(req) {
5-
libxmljs.parseXml(req.param("some-xml")); // $ Alert - libxml expands internal general entities by default
5+
libxmljs.parseXml(req.param("some-xml"));
66
});

javascript/ql/test/query-tests/Security/CWE-776/libxml.noent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ const express = require('express');
22
const libxmljs = require('libxmljs');
33

44
express().get('/some/path', function(req) {
5-
libxmljs.parseXml(req.param("some-xml"), { noent: true }); // $ Alert - unguarded entity expansion
5+
libxmljs.parseXml(req.param("some-xml"), { noent: true });
66
});

javascript/ql/test/query-tests/Security/CWE-776/libxml.sax.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ const libxmljs = require('libxmljs');
33

44
express().get('/some/path', function(req) {
55
const parser = new libxmljs.SaxParser();
6-
parser.parseString(req.param("some-xml")); // $ Alert - the SAX parser expands external entities by default
6+
parser.parseString(req.param("some-xml"));
77
});

javascript/ql/test/query-tests/Security/CWE-776/libxml.saxpush.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ const libxmljs = require('libxmljs');
33

44
express().get('/some/path', function(req) {
55
const parser = new libxmljs.SaxPushParser();
6-
parser.push(req.param("some-xml")); // $ Alert - the SAX parser expands external entities by default
6+
parser.push(req.param("some-xml"));
77
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Removed `lxml` as an XML bomb sink. The underlying libxml2 library now includes [entity reference loop detection](https://github.com/lxml/lxml/blob/f33ac2c2f5f9c4c4c1fc47f363be96db308f2fa6/doc/FAQ.txt#L1077) that prevents XML bomb attacks.

python/ql/lib/semmle/python/frameworks/Lxml.qll

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,6 @@ module Lxml {
129129
any(True t)
130130
)
131131
or
132-
kind.isXmlBomb() and
133-
this.getKeywordParameter("huge_tree").getAValueReachingSink().asExpr() = any(True t) and
134-
not this.getKeywordParameter("resolve_entities").getAValueReachingSink().asExpr() =
135-
any(False t)
136-
or
137132
kind.isDtdRetrieval() and
138133
this.getKeywordParameter("load_dtd").getAValueReachingSink().asExpr() = any(True t) and
139134
this.getKeywordParameter("no_network").getAValueReachingSink().asExpr() = any(False t)
@@ -305,9 +300,8 @@ module Lxml {
305300
// note that there is no `resolve_entities` argument, so it's not possible to turn off XXE :O
306301
kind.isXxe()
307302
or
308-
kind.isXmlBomb() and
309-
this.getKeywordParameter("huge_tree").getAValueReachingSink().asExpr() = any(True t)
310-
or
303+
// libxml2 has built-in protection against XML bombs via entity reference loop detection,
304+
// so lxml is not vulnerable to XML bomb attacks.
311305
kind.isDtdRetrieval() and
312306
this.getKeywordParameter("load_dtd").getAValueReachingSink().asExpr() = any(True t) and
313307
this.getKeywordParameter("no_network").getAValueReachingSink().asExpr() = any(False t)

python/ql/test/library-tests/frameworks/lxml/parsing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050

5151
# Billion laughs vuln (also XXE)
5252
parser = lxml.etree.XMLParser(huge_tree=True)
53-
lxml.etree.fromstring(x, parser=parser) # $ decodeFormat=XML decodeInput=x xmlVuln='XML bomb' xmlVuln='XXE' decodeOutput=lxml.etree.fromstring(..)
53+
lxml.etree.fromstring(x, parser=parser) # $ decodeFormat=XML decodeInput=x xmlVuln='XXE' decodeOutput=lxml.etree.fromstring(..)
5454

5555
# Safe for both Billion laughs and XXE
5656
parser = lxml.etree.XMLParser(resolve_entities=False, huge_tree=True)
@@ -63,5 +63,5 @@
6363
# iterparse configurations ... this doesn't use a parser argument but takes MOST (!) of
6464
# the normal XMLParser arguments. Specifically, it doesn't allow disabling XXE :O
6565

66-
lxml.etree.iterparse(xml_file, huge_tree=True) # $ decodeFormat=XML decodeInput=xml_file xmlVuln='XML bomb' xmlVuln='XXE' decodeOutput=lxml.etree.iterparse(..) getAPathArgument=xml_file
66+
lxml.etree.iterparse(xml_file, huge_tree=True) # $ decodeFormat=XML decodeInput=xml_file xmlVuln='XXE' decodeOutput=lxml.etree.iterparse(..) getAPathArgument=xml_file
6767
lxml.etree.iterparse(xml_file, load_dtd=True, no_network=False) # $ decodeFormat=XML decodeInput=xml_file xmlVuln='DTD retrieval' xmlVuln='XXE' decodeOutput=lxml.etree.iterparse(..) getAPathArgument=xml_file

0 commit comments

Comments
 (0)