From d6ff6b74c5d55134c4e31e74f5b44a4bef5dd05a Mon Sep 17 00:00:00 2001
From: intrigus
Date: Thu, 19 Mar 2020 22:26:37 +0100
Subject: [PATCH 1/7] CWE-643 XPathInjection on Go
---
.../Security/CWE-643/XPathInjection.go | 19 +++
.../Security/CWE-643/XPathInjection.qhelp | 45 ++++++
.../Security/CWE-643/XPathInjection.ql | 143 ++++++++++++++++++
3 files changed, 207 insertions(+)
create mode 100644 ql/src/experimental/Security/CWE-643/XPathInjection.go
create mode 100644 ql/src/experimental/Security/CWE-643/XPathInjection.qhelp
create mode 100644 ql/src/experimental/Security/CWE-643/XPathInjection.ql
diff --git a/ql/src/experimental/Security/CWE-643/XPathInjection.go b/ql/src/experimental/Security/CWE-643/XPathInjection.go
new file mode 100644
index 000000000..5d51fa328
--- /dev/null
+++ b/ql/src/experimental/Security/CWE-643/XPathInjection.go
@@ -0,0 +1,19 @@
+package main
+
+import (
+ "net/http"
+ "github.com/moovweb/gokogiri"
+)
+
+func processRequest(r *http.Request, doc *XmlDocument) {
+ r.parseForm()
+ username := r.Form.Get("username")
+ password := r.Form.Get("password")
+
+ root := doc.Root()
+ // BAD: User input used directly in an XPath expression
+ doc, _ := root.SearchWithVariables("//users/user[login/text()='" + username + "' and password/text() = '" + password + "']/home_dir/text()")
+
+ // GOOD: Uses parameters to avoid including user input directly in XPath expression
+ doc, _ := root.SearchWithVariables("//users/user[login/text()=$username and password/text() = $password]/home_dir/text()")
+}
diff --git a/ql/src/experimental/Security/CWE-643/XPathInjection.qhelp b/ql/src/experimental/Security/CWE-643/XPathInjection.qhelp
new file mode 100644
index 000000000..4996733a3
--- /dev/null
+++ b/ql/src/experimental/Security/CWE-643/XPathInjection.qhelp
@@ -0,0 +1,45 @@
+
+
+
+
+If an XPath expression is built using string concatenation, and the components of the concatenation
+include user input, a user is likely to be able to create a malicious XPath expression.
+
+
+
+
+
+If user input must be included in an XPath expression, pre-compile the query and use variable
+references to include the user input.
+
+
+For exmaple, when using the github.com/moovweb/gokogiri
API, this can be done by creating a custom subtype of
+xpath.VariableScope
, and implementing
+ResolveVariable(string, string)
to return the user provided data. This
+custom scope can be specified when calling SearchWithVariables(), EvalXPath() or EvalXPathAsBoolean()
.
+
+
+
+
+
+
+In the first example, the code accepts a user name specified by the user, and uses this
+unvalidated and unsanitized value in an XPath expression. This is vulnerable to the user providing
+special characters or string sequences that change the meaning of the XPath expression to search
+for different values.
+
+
+
+In the second example, the XPath expression is a hard-coded string that specifies some variables,
+which are safely replaced at runtime using a custom xpath.VariableScope
.
+
+
+
+
+
+OWASP: Testing for XPath Injection.
+OWASP: XPath Injection.
+
+
diff --git a/ql/src/experimental/Security/CWE-643/XPathInjection.ql b/ql/src/experimental/Security/CWE-643/XPathInjection.ql
new file mode 100644
index 000000000..2e34aaca3
--- /dev/null
+++ b/ql/src/experimental/Security/CWE-643/XPathInjection.ql
@@ -0,0 +1,143 @@
+/**
+ * @name XPath injection
+ * @description Building an XPath expression from user-controlled sources is vulnerable to insertion of
+ * malicious code by the user.
+ * @kind path-problem
+ * @problem.severity error
+ * @precision high
+ * @id go/xml/xpath-injection
+ * @tags security
+ * external/cwe/cwe-643
+ */
+
+import go
+import semmle.go.dataflow.TaintTracking
+import DataFlow::PathGraph
+
+class XPathInjectionConfiguration extends TaintTracking::Configuration {
+ XPathInjectionConfiguration() { this = "XPathInjectionConfiguration" }
+
+ override predicate isSource(DataFlow::Node source) {
+ source instanceof UntrustedFlowSource
+ }
+
+ override predicate isSink(DataFlow::Node sink) { sink instanceof XPathInjectionSink }
+}
+
+abstract class XPathInjectionSink extends DataFlow::Node { }
+
+Function getAMatchingFunction(string package, int argumentNumber, CallExpr call, Expr argument) {
+ result.getPackage().getName() = package and
+ call.getTarget() = result and
+ call.getArgument(argumentNumber) = argument
+}
+
+// https://github.com/antchfx/xpath
+class XPathSink extends XPathInjectionSink {
+ XPathSink() {
+ exists(CallExpr call |
+ getAMatchingFunction("xpath", 0, call, this.asExpr()).getName().matches("Compile")
+ or
+ getAMatchingFunction("xpath", 0, call, this.asExpr()).getName().matches("MustCompile")
+ or
+ getAMatchingFunction("xpath", 1, call, this.asExpr()).getName().matches("Select")
+ )
+ }
+}
+
+// https://github.com/antchfx/htmlquery
+class HtmlQuerySink extends XPathInjectionSink {
+ HtmlQuerySink() {
+ exists(CallExpr call |
+ getAMatchingFunction("htmlquery", 1, call, this.asExpr()).getName().matches("Find%")
+ or
+ getAMatchingFunction("htmlquery", 1, call, this.asExpr()).getName().matches("Query%")
+ or
+ getAMatchingFunction("htmlquery", 0, call, this.asExpr()).getName().matches("getQuery")
+ )
+ }
+}
+
+// https://github.com/antchfx/xmlquery
+class XmlQuerySink extends XPathInjectionSink {
+ XmlQuerySink() {
+ exists(CallExpr call |
+ getAMatchingFunction("xmlquery", 1, call, this.asExpr()).getName().matches("Find%")
+ or
+ getAMatchingFunction("xmlquery", 1, call, this.asExpr()).getName().matches("Query%")
+ or
+ getAMatchingFunction("xmlquery", 0, call, this.asExpr()).getName().matches("getQuery")
+ or
+ getAMatchingFunction("xmlquery", 0, call, this.asExpr()).getName().matches("Select%")
+ )
+ }
+}
+
+// https://github.com/antchfx/jsonquery
+class JsonQuerySink extends XPathInjectionSink {
+ JsonQuerySink() {
+ exists(CallExpr call |
+ getAMatchingFunction("jsonquery", 1, call, this.asExpr()).getName().matches("Find%")
+ or
+ getAMatchingFunction("jsonquery", 1, call, this.asExpr()).getName().matches("Query%")
+ or
+ getAMatchingFunction("jsonquery", 0, call, this.asExpr()).getName().matches("getQuery")
+ )
+ }
+}
+
+// https://github.com/go-xmlpath/xmlpath
+class XmlPathSink extends XPathInjectionSink {
+ XmlPathSink() {
+ exists(CallExpr call |
+ getAMatchingFunction("xmlpath", 0, call, this.asExpr()).getName().matches("Compile%")
+ or
+ getAMatchingFunction("xmlpath", 0, call, this.asExpr()).getName().matches("MustCompile")
+ )
+ }
+}
+
+// https://github.com/ChrisTrenkamp/goxpath
+class GoXPathSink extends XPathInjectionSink {
+ GoXPathSink() {
+ exists(CallExpr call |
+ getAMatchingFunction("goxpath", 0, call, this.asExpr()).getName().matches("Parse")
+ or
+ getAMatchingFunction("goxpath", 0, call, this.asExpr()).getName().matches("MustParse")
+ or
+ getAMatchingFunction("goxpath", 0, call, this.asExpr()).getName().matches("ParseExec")
+ )
+ }
+}
+
+// https://github.com/santhosh-tekuri/xpathparser
+class XPathParserSink extends XPathInjectionSink {
+ XPathParserSink() {
+ exists(CallExpr call |
+ getAMatchingFunction("xpathparser", 0, call, this.asExpr()).getName().matches("Parse")
+ or
+ getAMatchingFunction("xpathparser", 0, call, this.asExpr()).getName().matches("MustParse")
+ )
+ }
+}
+
+// https://github.com/moovweb/gokogiri
+class GokogiriSink extends XPathInjectionSink {
+ GokogiriSink() {
+ exists(CallExpr call |
+ getAMatchingFunction("xpath", 0, call, this.asExpr()).getName().matches("Compile")
+ or
+ // TODO: The following cases will have false positives in case a *xpath.Expression is supplied
+ // I don't know how to fix this, since I'm new to the Go QL flavour.
+ // https://github.com/moovweb/gokogiri/blob/a1a828153468a7518b184e698f6265904108d957/xml/node.go#L613
+ getAMatchingFunction("xml", 0, call, this.asExpr()).getName().matches("Search%")
+ or
+ getAMatchingFunction("xml", 0, call, this.asExpr()).getName().matches("EvalXPath%")
+ )
+ }
+}
+
+from DataFlow::PathNode source, DataFlow::PathNode sink, XPathInjectionConfiguration c
+where c.hasFlowPath(source, sink)
+select sink.getNode(), source, sink, "$@ flows to here and is used in an XPath expression.",
+ source.getNode(), "User-provided value"
From ec40cf0379cbbadafa84c54428a3ac218ca969d3 Mon Sep 17 00:00:00 2001
From: intrigus-lgtm <60750685+intrigus-lgtm@users.noreply.github.com>
Date: Fri, 20 Mar 2020 13:55:31 +0100
Subject: [PATCH 2/7] Apply suggestions from review
Co-Authored-By: Max Schaefer <54907921+max-schaefer@users.noreply.github.com>
---
ql/src/experimental/Security/CWE-643/XPathInjection.qhelp | 4 ++--
ql/src/experimental/Security/CWE-643/XPathInjection.ql | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/ql/src/experimental/Security/CWE-643/XPathInjection.qhelp b/ql/src/experimental/Security/CWE-643/XPathInjection.qhelp
index 4996733a3..91bd0ca9e 100644
--- a/ql/src/experimental/Security/CWE-643/XPathInjection.qhelp
+++ b/ql/src/experimental/Security/CWE-643/XPathInjection.qhelp
@@ -15,10 +15,10 @@ If user input must be included in an XPath expression, pre-compile the query and
references to include the user input.
-For exmaple, when using the github.com/moovweb/gokogiri
API, this can be done by creating a custom subtype of
+For example, when using the github.com/moovweb/gokogiri
API, this can be done by creating a custom subtype of
xpath.VariableScope
, and implementing
ResolveVariable(string, string)
to return the user provided data. This
-custom scope can be specified when calling SearchWithVariables(), EvalXPath() or EvalXPathAsBoolean()
.
+custom scope can be specified when calling SearchWithVariables()
, EvalXPath()
, or EvalXPathAsBoolean()
.
diff --git a/ql/src/experimental/Security/CWE-643/XPathInjection.ql b/ql/src/experimental/Security/CWE-643/XPathInjection.ql
index 2e34aaca3..f24aefa82 100644
--- a/ql/src/experimental/Security/CWE-643/XPathInjection.ql
+++ b/ql/src/experimental/Security/CWE-643/XPathInjection.ql
@@ -139,5 +139,5 @@ class GokogiriSink extends XPathInjectionSink {
from DataFlow::PathNode source, DataFlow::PathNode sink, XPathInjectionConfiguration c
where c.hasFlowPath(source, sink)
-select sink.getNode(), source, sink, "$@ flows to here and is used in an XPath expression.",
- source.getNode(), "User-provided value"
+select sink.getNode(), source, sink, "$@ flows here and is used in an XPath expression.",
+ source.getNode(), "A user-provided value"
From c7ead88b91dcf1c6a0d07c4d5091256a62b47ff0 Mon Sep 17 00:00:00 2001
From: intrigus
Date: Fri, 20 Mar 2020 20:07:28 +0100
Subject: [PATCH 3/7] Restructure query, add default sanitizer
---
.../Security/CWE-643/XPathInjection.ql | 167 +++++++++++-------
1 file changed, 106 insertions(+), 61 deletions(-)
diff --git a/ql/src/experimental/Security/CWE-643/XPathInjection.ql b/ql/src/experimental/Security/CWE-643/XPathInjection.ql
index f24aefa82..985222715 100644
--- a/ql/src/experimental/Security/CWE-643/XPathInjection.ql
+++ b/ql/src/experimental/Security/CWE-643/XPathInjection.ql
@@ -11,36 +11,43 @@
*/
import go
-import semmle.go.dataflow.TaintTracking
import DataFlow::PathGraph
+class ByteSliceType extends SliceType {
+ ByteSliceType() { this.getElementType() instanceof Uint8Type }
+}
+
class XPathInjectionConfiguration extends TaintTracking::Configuration {
XPathInjectionConfiguration() { this = "XPathInjectionConfiguration" }
- override predicate isSource(DataFlow::Node source) {
- source instanceof UntrustedFlowSource
- }
+ override predicate isSource(DataFlow::Node source) { source instanceof UntrustedFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof XPathInjectionSink }
+
+ override predicate isSanitizer(DataFlow::Node node) {
+ not node.asExpr().getType() instanceof StringType or
+ not node.asExpr().getType() instanceof ByteSliceType
+ }
}
abstract class XPathInjectionSink extends DataFlow::Node { }
-Function getAMatchingFunction(string package, int argumentNumber, CallExpr call, Expr argument) {
- result.getPackage().getName() = package and
- call.getTarget() = result and
- call.getArgument(argumentNumber) = argument
-}
-
// https://github.com/antchfx/xpath
class XPathSink extends XPathInjectionSink {
XPathSink() {
- exists(CallExpr call |
- getAMatchingFunction("xpath", 0, call, this.asExpr()).getName().matches("Compile")
- or
- getAMatchingFunction("xpath", 0, call, this.asExpr()).getName().matches("MustCompile")
- or
- getAMatchingFunction("xpath", 1, call, this.asExpr()).getName().matches("Select")
+ exists(Function f |
+ f.hasQualifiedName("github.com/antchfx/xpath", "Compile%") and
+ this = f.getACall().getArgument(0)
+ )
+ or
+ exists(Function f |
+ f.hasQualifiedName("github.com/antchfx/xpath", "MustCompile%") and
+ this = f.getACall().getArgument(0)
+ )
+ or
+ exists(Function f |
+ f.hasQualifiedName("github.com/antchfx/xpath", "Select%") and
+ this = f.getACall().getArgument(1)
)
}
}
@@ -48,12 +55,19 @@ class XPathSink extends XPathInjectionSink {
// https://github.com/antchfx/htmlquery
class HtmlQuerySink extends XPathInjectionSink {
HtmlQuerySink() {
- exists(CallExpr call |
- getAMatchingFunction("htmlquery", 1, call, this.asExpr()).getName().matches("Find%")
- or
- getAMatchingFunction("htmlquery", 1, call, this.asExpr()).getName().matches("Query%")
- or
- getAMatchingFunction("htmlquery", 0, call, this.asExpr()).getName().matches("getQuery")
+ exists(Function f, string name | name.matches("Find%") |
+ f.hasQualifiedName("github.com/antchfx/htmlquery", name) and
+ this = f.getACall().getArgument(1)
+ )
+ or
+ exists(Function f, string name | name.matches("Query%") |
+ f.hasQualifiedName("github.com/antchfx/htmlquery", name) and
+ this = f.getACall().getArgument(1)
+ )
+ or
+ exists(Function f |
+ f.hasQualifiedName("github.com/antchfx/htmlquery", "getQuery") and
+ this = f.getACall().getArgument(0)
)
}
}
@@ -61,14 +75,24 @@ class HtmlQuerySink extends XPathInjectionSink {
// https://github.com/antchfx/xmlquery
class XmlQuerySink extends XPathInjectionSink {
XmlQuerySink() {
- exists(CallExpr call |
- getAMatchingFunction("xmlquery", 1, call, this.asExpr()).getName().matches("Find%")
- or
- getAMatchingFunction("xmlquery", 1, call, this.asExpr()).getName().matches("Query%")
- or
- getAMatchingFunction("xmlquery", 0, call, this.asExpr()).getName().matches("getQuery")
- or
- getAMatchingFunction("xmlquery", 0, call, this.asExpr()).getName().matches("Select%")
+ exists(Function f, string name | name.matches("Find%") |
+ f.hasQualifiedName("github.com/antchfx/xmlquery", name) and
+ this = f.getACall().getArgument(1)
+ )
+ or
+ exists(Function f, string name | name.matches("Query%") |
+ f.hasQualifiedName("github.com/antchfx/xmlquery", name) and
+ this = f.getACall().getArgument(1)
+ )
+ or
+ exists(Function f, string name | name.matches("Select%") |
+ f.hasQualifiedName("github.com/antchfx/xmlquery", name) and
+ this = f.getACall().getArgument(0)
+ )
+ or
+ exists(Function f |
+ f.hasQualifiedName("github.com/antchfx/xmlquery", "getQuery") and
+ this = f.getACall().getArgument(0)
)
}
}
@@ -76,12 +100,19 @@ class XmlQuerySink extends XPathInjectionSink {
// https://github.com/antchfx/jsonquery
class JsonQuerySink extends XPathInjectionSink {
JsonQuerySink() {
- exists(CallExpr call |
- getAMatchingFunction("jsonquery", 1, call, this.asExpr()).getName().matches("Find%")
- or
- getAMatchingFunction("jsonquery", 1, call, this.asExpr()).getName().matches("Query%")
- or
- getAMatchingFunction("jsonquery", 0, call, this.asExpr()).getName().matches("getQuery")
+ exists(Function f, string name | name.matches("Find%") |
+ f.hasQualifiedName("github.com/antchfx/jsonquery", name) and
+ this = f.getACall().getArgument(1)
+ )
+ or
+ exists(Function f, string name | name.matches("Query%") |
+ f.hasQualifiedName("github.com/antchfx/jsonquery", name) and
+ this = f.getACall().getArgument(1)
+ )
+ or
+ exists(Function f |
+ f.hasQualifiedName("github.com/antchfx/jsonquery", "getQuery") and
+ this = f.getACall().getArgument(0)
)
}
}
@@ -89,10 +120,14 @@ class JsonQuerySink extends XPathInjectionSink {
// https://github.com/go-xmlpath/xmlpath
class XmlPathSink extends XPathInjectionSink {
XmlPathSink() {
- exists(CallExpr call |
- getAMatchingFunction("xmlpath", 0, call, this.asExpr()).getName().matches("Compile%")
- or
- getAMatchingFunction("xmlpath", 0, call, this.asExpr()).getName().matches("MustCompile")
+ exists(Function f, string name | name.matches("Compile%") |
+ f.hasQualifiedName("github.com/go-xmlpath/xmlpath", name) and
+ this = f.getACall().getArgument(0)
+ )
+ or
+ exists(Function f, string name | name.matches("MustCompile%") |
+ f.hasQualifiedName("github.com/go-xmlpath/xmlpath", name) and
+ this = f.getACall().getArgument(0)
)
}
}
@@ -100,12 +135,14 @@ class XmlPathSink extends XPathInjectionSink {
// https://github.com/ChrisTrenkamp/goxpath
class GoXPathSink extends XPathInjectionSink {
GoXPathSink() {
- exists(CallExpr call |
- getAMatchingFunction("goxpath", 0, call, this.asExpr()).getName().matches("Parse")
- or
- getAMatchingFunction("goxpath", 0, call, this.asExpr()).getName().matches("MustParse")
- or
- getAMatchingFunction("goxpath", 0, call, this.asExpr()).getName().matches("ParseExec")
+ exists(Function f, string name | name.matches("Parse%") |
+ f.hasQualifiedName("github.com/ChrisTrenkamp/goxpath", name) and
+ this = f.getACall().getArgument(0)
+ )
+ or
+ exists(Function f, string name | name.matches("MustParse%") |
+ f.hasQualifiedName("github.com/ChrisTrenkamp/goxpath", name) and
+ this = f.getACall().getArgument(0)
)
}
}
@@ -113,10 +150,14 @@ class GoXPathSink extends XPathInjectionSink {
// https://github.com/santhosh-tekuri/xpathparser
class XPathParserSink extends XPathInjectionSink {
XPathParserSink() {
- exists(CallExpr call |
- getAMatchingFunction("xpathparser", 0, call, this.asExpr()).getName().matches("Parse")
- or
- getAMatchingFunction("xpathparser", 0, call, this.asExpr()).getName().matches("MustParse")
+ exists(Function f, string name | name.matches("Parse%") |
+ f.hasQualifiedName("github.com/santhosh-tekuri/xpathparser", name) and
+ this = f.getACall().getArgument(0)
+ )
+ or
+ exists(Function f, string name | name.matches("MustParse%") |
+ f.hasQualifiedName("github.com/santhosh-tekuri/xpathparser", name) and
+ this = f.getACall().getArgument(0)
)
}
}
@@ -124,20 +165,24 @@ class XPathParserSink extends XPathInjectionSink {
// https://github.com/moovweb/gokogiri
class GokogiriSink extends XPathInjectionSink {
GokogiriSink() {
- exists(CallExpr call |
- getAMatchingFunction("xpath", 0, call, this.asExpr()).getName().matches("Compile")
- or
- // TODO: The following cases will have false positives in case a *xpath.Expression is supplied
- // I don't know how to fix this, since I'm new to the Go QL flavour.
- // https://github.com/moovweb/gokogiri/blob/a1a828153468a7518b184e698f6265904108d957/xml/node.go#L613
- getAMatchingFunction("xml", 0, call, this.asExpr()).getName().matches("Search%")
- or
- getAMatchingFunction("xml", 0, call, this.asExpr()).getName().matches("EvalXPath%")
+ exists(Function f, string name | name.matches("Compile%") |
+ f.hasQualifiedName("github.com/moovweb/gokogiri/xpath", name) and
+ this = f.getACall().getArgument(0)
+ )
+ or
+ exists(Function f, string name | name.matches("Search%") |
+ f.hasQualifiedName("github.com/moovweb/gokogiri/xml", name) and
+ this = f.getACall().getArgument(0)
+ )
+ or
+ exists(Function f, string name | name.matches("EvalXPath%") |
+ f.hasQualifiedName("github.com/moovweb/gokogiri/xml", name) and
+ this = f.getACall().getArgument(0)
)
}
}
-from DataFlow::PathNode source, DataFlow::PathNode sink, XPathInjectionConfiguration c
-where c.hasFlowPath(source, sink)
+from DataFlow::PathNode source, DataFlow::PathNode sink, XPathInjectionConfiguration c, Function f
+where c.hasFlowPath(source, sink) and f.getName().matches("Compile%")
select sink.getNode(), source, sink, "$@ flows here and is used in an XPath expression.",
source.getNode(), "A user-provided value"
From 948b79df876ba58858656feb9fd5426236190816 Mon Sep 17 00:00:00 2001
From: intrigus
Date: Fri, 20 Mar 2020 21:09:28 +0100
Subject: [PATCH 4/7] Update xpath example, use goxpath package
---
.../Security/CWE-643/XPathInjection.go | 37 +++++++++++++------
1 file changed, 25 insertions(+), 12 deletions(-)
diff --git a/ql/src/experimental/Security/CWE-643/XPathInjection.go b/ql/src/experimental/Security/CWE-643/XPathInjection.go
index 5d51fa328..869c98acb 100644
--- a/ql/src/experimental/Security/CWE-643/XPathInjection.go
+++ b/ql/src/experimental/Security/CWE-643/XPathInjection.go
@@ -1,19 +1,32 @@
package main
import (
- "net/http"
- "github.com/moovweb/gokogiri"
+ "fmt"
+ "net/http"
+
+ "github.com/ChrisTrenkamp/goxpath"
+ "github.com/ChrisTrenkamp/goxpath/tree"
)
-func processRequest(r *http.Request, doc *XmlDocument) {
- r.parseForm()
- username := r.Form.Get("username")
- password := r.Form.Get("password")
-
- root := doc.Root()
- // BAD: User input used directly in an XPath expression
- doc, _ := root.SearchWithVariables("//users/user[login/text()='" + username + "' and password/text() = '" + password + "']/home_dir/text()")
+func main() {}
+
+func processRequest(r *http.Request, doc tree.Node) {
+ r.ParseForm()
+ username := r.Form.Get("username")
+ password := r.Form.Get("password")
+
+ // BAD: User input used directly in an XPath expression
+ xPath := goxpath.MustParse("//users/user[login/text()='" + username + "' and password/text() = '" + password + "']/home_dir/text()")
+ unsafeRes, _ := xPath.ExecBool(doc)
+ fmt.Println(unsafeRes)
- // GOOD: Uses parameters to avoid including user input directly in XPath expression
- doc, _ := root.SearchWithVariables("//users/user[login/text()=$username and password/text() = $password]/home_dir/text()")
+ // GOOD: Value of parameters is defined here instead of directly in the query
+ opt := func(o *goxpath.Opts) {
+ o.Vars["username"] = tree.String(username)
+ o.Vars["password"] = tree.String(password)
+ }
+ // GOOD: Uses parameters to avoid including user input directly in XPath expression
+ xPath = goxpath.MustParse("//users/user[login/text()=$username and password/text() = $password]/home_dir/text()")
+ safeRes, _ := xPath.ExecBool(doc, opt)
+ fmt.Println(safeRes)
}
From d81c9b145e825f2f779287404941df5b2ea01a8c Mon Sep 17 00:00:00 2001
From: intrigus
Date: Fri, 20 Mar 2020 21:32:12 +0100
Subject: [PATCH 5/7] Update query help to use goxpath
---
.../experimental/Security/CWE-643/XPathInjection.qhelp | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/ql/src/experimental/Security/CWE-643/XPathInjection.qhelp b/ql/src/experimental/Security/CWE-643/XPathInjection.qhelp
index 91bd0ca9e..23d693ac7 100644
--- a/ql/src/experimental/Security/CWE-643/XPathInjection.qhelp
+++ b/ql/src/experimental/Security/CWE-643/XPathInjection.qhelp
@@ -15,10 +15,9 @@ If user input must be included in an XPath expression, pre-compile the query and
references to include the user input.
-For example, when using the github.com/moovweb/gokogiri
API, this can be done by creating a custom subtype of
-xpath.VariableScope
, and implementing
-ResolveVariable(string, string)
to return the user provided data. This
-custom scope can be specified when calling SearchWithVariables()
, EvalXPath()
, or EvalXPathAsBoolean()
.
+For example, when using the github.com/ChrisTrenkamp/goxpath
API, this can be done by creating a function that takes an *goxpath.Opts
structure.
+In this structure you can then set the values of the variable references.
+This function can then be specified when calling Exec()
, Exec{Bool|Num|Node}()
, ParseExec()
or MustExec()
.
@@ -33,7 +32,7 @@ for different values.
In the second example, the XPath expression is a hard-coded string that specifies some variables,
-which are safely replaced at runtime using a custom xpath.VariableScope
.
+which are safely resolved at runtime using the goxpath.Opts
structure.
From 9187bacd3c5b1b21e2200e047af95b86a6701456 Mon Sep 17 00:00:00 2001
From: intrigus-lgtm <60750685+intrigus-lgtm@users.noreply.github.com>
Date: Mon, 23 Mar 2020 16:45:56 +0100
Subject: [PATCH 6/7] Apply suggestion from code review
Use getUnderlyingType() to account for named aliases.
Co-Authored-By: Max Schaefer <54907921+max-schaefer@users.noreply.github.com>
---
ql/src/experimental/Security/CWE-643/XPathInjection.ql | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/ql/src/experimental/Security/CWE-643/XPathInjection.ql b/ql/src/experimental/Security/CWE-643/XPathInjection.ql
index 985222715..b0b00062e 100644
--- a/ql/src/experimental/Security/CWE-643/XPathInjection.ql
+++ b/ql/src/experimental/Security/CWE-643/XPathInjection.ql
@@ -25,8 +25,9 @@ class XPathInjectionConfiguration extends TaintTracking::Configuration {
override predicate isSink(DataFlow::Node sink) { sink instanceof XPathInjectionSink }
override predicate isSanitizer(DataFlow::Node node) {
- not node.asExpr().getType() instanceof StringType or
- not node.asExpr().getType() instanceof ByteSliceType
+ exists(Type t | t = node.getType().getUnderlyingType() |
+ not t instanceof StringType or not t instanceof ByteSliceType
+ )
}
}
From 1f635806b35d684ef72848d713a25f7ae2fc56bc Mon Sep 17 00:00:00 2001
From: intrigus
Date: Mon, 23 Mar 2020 16:49:45 +0100
Subject: [PATCH 7/7] Fix copy-paste errors, remove debugging code
---
.../Security/CWE-643/XPathInjection.ql | 23 +++++++++----------
1 file changed, 11 insertions(+), 12 deletions(-)
diff --git a/ql/src/experimental/Security/CWE-643/XPathInjection.ql b/ql/src/experimental/Security/CWE-643/XPathInjection.ql
index b0b00062e..453710d71 100644
--- a/ql/src/experimental/Security/CWE-643/XPathInjection.ql
+++ b/ql/src/experimental/Security/CWE-643/XPathInjection.ql
@@ -4,7 +4,6 @@
* malicious code by the user.
* @kind path-problem
* @problem.severity error
- * @precision high
* @id go/xml/xpath-injection
* @tags security
* external/cwe/cwe-643
@@ -25,9 +24,9 @@ class XPathInjectionConfiguration extends TaintTracking::Configuration {
override predicate isSink(DataFlow::Node sink) { sink instanceof XPathInjectionSink }
override predicate isSanitizer(DataFlow::Node node) {
- exists(Type t | t = node.getType().getUnderlyingType() |
- not t instanceof StringType or not t instanceof ByteSliceType
- )
+ exists(Type t | t = node.getType().getUnderlyingType() |
+ not t instanceof StringType or not t instanceof ByteSliceType
+ )
}
}
@@ -36,18 +35,18 @@ abstract class XPathInjectionSink extends DataFlow::Node { }
// https://github.com/antchfx/xpath
class XPathSink extends XPathInjectionSink {
XPathSink() {
- exists(Function f |
- f.hasQualifiedName("github.com/antchfx/xpath", "Compile%") and
+ exists(Function f, string name | name.matches("Compile%") |
+ f.hasQualifiedName("github.com/antchfx/xpath", name) and
this = f.getACall().getArgument(0)
)
or
- exists(Function f |
- f.hasQualifiedName("github.com/antchfx/xpath", "MustCompile%") and
+ exists(Function f, string name | name.matches("MustCompile%") |
+ f.hasQualifiedName("github.com/antchfx/xpath", name) and
this = f.getACall().getArgument(0)
)
or
- exists(Function f |
- f.hasQualifiedName("github.com/antchfx/xpath", "Select%") and
+ exists(Function f, string name | name.matches("Select%") |
+ f.hasQualifiedName("github.com/antchfx/xpath", name) and
this = f.getACall().getArgument(1)
)
}
@@ -183,7 +182,7 @@ class GokogiriSink extends XPathInjectionSink {
}
}
-from DataFlow::PathNode source, DataFlow::PathNode sink, XPathInjectionConfiguration c, Function f
-where c.hasFlowPath(source, sink) and f.getName().matches("Compile%")
+from DataFlow::PathNode source, DataFlow::PathNode sink, XPathInjectionConfiguration c
+where c.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ flows here and is used in an XPath expression.",
source.getNode(), "A user-provided value"