diff --git a/UPGRADING.md b/UPGRADING.md
index d47bbf45d..4b3b3c6c9 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -11,6 +11,8 @@
- [BC] The `TDependentListKey` type was removed and replaced with an optional property of the `TIntRange` type.
+- [BC] Value of constant `Psalm\Type\TaintKindGroup::ALL_INPUT` changed to reflect a new `TaintKind::INPUT_XPATH` have been added. Accordingly, default values for `$taint` parameters of `Psalm\Codebase::addTaintSource()` and `Psalm\Codebase::addTaintSink()` have been changed as well.
+
- [BC] Property `Config::$shepherd_host` was replaced with `Config::$shepherd_endpoint`
- [BC] Methods `Codebase::getSymbolLocation()` and `Codebase::getSymbolInformation()` were replaced with `Codebase::getSymbolLocationByReference()`
diff --git a/config.xsd b/config.xsd
index 4cf075b6e..eb5f11e2c 100644
--- a/config.xsd
+++ b/config.xsd
@@ -444,6 +444,7 @@
+
diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md
index 55a18b8fa..90b5d5351 100644
--- a/docs/running_psalm/error_levels.md
+++ b/docs/running_psalm/error_levels.md
@@ -297,6 +297,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even
- [TaintedSystemSecret](issues/TaintedSystemSecret.md)
- [TaintedUnserialize](issues/TaintedUnserialize.md)
- [TaintedUserSecret](issues/TaintedUserSecret.md)
+ - [TaintedXpath](issues/TaintedXpath.md)
- [UncaughtThrowInGlobalScope](issues/UncaughtThrowInGlobalScope.md)
- [UnevaluatedCode](issues/UnevaluatedCode.md)
- [UnnecessaryVarAnnotation](issues/UnnecessaryVarAnnotation.md)
diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md
index d9b3b4f16..592225002 100644
--- a/docs/running_psalm/issues.md
+++ b/docs/running_psalm/issues.md
@@ -246,6 +246,7 @@
- [TaintedTextWithQuotes](issues/TaintedTextWithQuotes.md)
- [TaintedUnserialize](issues/TaintedUnserialize.md)
- [TaintedUserSecret](issues/TaintedUserSecret.md)
+ - [TaintedXpath](issues/TaintedXpath.md)
- [TooFewArguments](issues/TooFewArguments.md)
- [TooManyArguments](issues/TooManyArguments.md)
- [TooManyTemplateParams](issues/TooManyTemplateParams.md)
diff --git a/docs/running_psalm/issues/TaintedXpath.md b/docs/running_psalm/issues/TaintedXpath.md
new file mode 100644
index 000000000..22616446a
--- /dev/null
+++ b/docs/running_psalm/issues/TaintedXpath.md
@@ -0,0 +1,12 @@
+# TaintedXpath
+
+Emitted when user-controlled input can be passed into to a xpath query.
+
+```php
+xpath($expression);
+}
+```
diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php
index b18b82eb1..ba4f20fcc 100644
--- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php
+++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php
@@ -24,6 +24,7 @@ use Psalm\Issue\TaintedSystemSecret;
use Psalm\Issue\TaintedTextWithQuotes;
use Psalm\Issue\TaintedUnserialize;
use Psalm\Issue\TaintedUserSecret;
+use Psalm\Issue\TaintedXpath;
use Psalm\IssueBuffer;
use Psalm\Type\TaintKind;
@@ -449,6 +450,15 @@ class TaintFlowGraph extends DataFlowGraph
);
break;
+ case TaintKind::INPUT_XPATH:
+ $issue = new TaintedXpath(
+ 'Detected tainted xpath query',
+ $issue_location,
+ $issue_trace,
+ $path,
+ );
+ break;
+
default:
$issue = new TaintedCustom(
'Detected tainted ' . $matching_taint,
diff --git a/src/Psalm/Issue/TaintedXpath.php b/src/Psalm/Issue/TaintedXpath.php
new file mode 100644
index 000000000..b9e4dbb42
--- /dev/null
+++ b/src/Psalm/Issue/TaintedXpath.php
@@ -0,0 +1,8 @@
+|false
+ * @psalm-taint-sink xpath $expression
+ */
public function evaluate(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed {}
/**
* @return DOMNodeList|false
+ * @psalm-taint-sink xpath $expression
*/
public function query(string $expression, ?DOMNode $contextNode = null, bool $registerNodeNS = true): mixed {}
diff --git a/stubs/extensions/simplexml.phpstub b/stubs/extensions/simplexml.phpstub
index 7f0bfa214..d2501f620 100644
--- a/stubs/extensions/simplexml.phpstub
+++ b/stubs/extensions/simplexml.phpstub
@@ -29,7 +29,10 @@ function simplexml_import_dom(SimpleXMLElement|DOMNode $node, ?string $class_nam
*/
class SimpleXMLElement implements Traversable, Countable
{
- /** @return array|null|false */
+ /**
+ * @return array|null|false
+ * @psalm-taint-sink xpath $expression
+ */
public function xpath(string $expression) {}
public function registerXPathNamespace(string $prefix, string $namespace): bool {}
diff --git a/tests/TaintTest.php b/tests/TaintTest.php
index d5dd0e1dc..7efd9dae9 100644
--- a/tests/TaintTest.php
+++ b/tests/TaintTest.php
@@ -750,6 +750,19 @@ class TaintTest extends TestCase
$mysqli->query("$a$b$c$d");',
],
+ 'querySimpleXMLElement' => [
+ 'code' => 'xpath($expression);
+ }',
+ ],
];
}
@@ -2503,6 +2516,30 @@ class TaintTest extends TestCase
$function->invoke();',
'error_message' => 'TaintedCallable',
],
+ 'querySimpleXMLElement' => [
+ 'code' => 'xpath($expression);
+ }',
+ 'error_message' => 'TaintedXpath',
+ ],
+ 'queryDOMXPath' => [
+ 'code' => 'query($expression);
+ }',
+ 'error_message' => 'TaintedXpath',
+ ],
+ 'evaluateDOMXPath' => [
+ 'code' => 'evaluate($expression);
+ }',
+ 'error_message' => 'TaintedXpath',
+ ],
];
}