Security Analysis in Psalm
Psalm can attempt to find connections between user-controlled input (like $_GET['name']) and places that we don’t want unescaped user-controlled input to end up (like echo "<h1>$name</h1>" by looking at the ways that data flows through your application (via assignments, function/method calls and array/property access).
Taint analysis runs by default, in the same run as type checking. To turn it off, set runTaintAnalysis="false" in psalm.xml; the --taint-analysis command line flag still turns it on for a single run.
Tainted input is anything that can be controlled, wholly or in part, by a user of your application. In taint analysis, tainted input is called a taint source.
Example sources:
$_GET[‘id’]$_POST['email']$_COOKIE['token']
Taint analysis tracks how data flows from taint sources into taint sinks. Taint sinks are places you really don’t want untrusted data to end up.
Example sinks:
<div id="section_<?= $id ?>">$pdo->exec("select * from users where name='" . $name . "'")
Taint Types
Psalm recognises a number of taint types by default, defined in the Psalm\Type\TaintKind class:
sql- used for strings that could contain SQLnosql- used for values that could set the structure of a NoSQL query (e.g. a MongoDB filter, update or pipeline). Only arrays and objects can carry it: a scalar can never be a query document, so casting user input tostring,int,floatorboolremoves itldap- used for strings that could contain a ldap DN or filterxpath- used for strings that could contain an XPath expressionhtml- used for strings that could contain angle brackets or unquoted stringshas_quotes- used for strings that could contain unquoted stringsshell- used for strings that could contain shell commandscallable- used for callable strings that could be user-controlledunserialize- used for strings that could contain a serialized stringinclude- used for strings that could contain a path being includedeval- used for strings that could contain codeextract- used for arrays that could be passed toextract(), overwriting variablesssrf- used for strings that could contain text passed to Curl or similarurl_component- used for strings that could contain URL syntax (/,?,&,#...) placed in the path, query or fragment of a URLurl_path- used for strings that could be a.or..segment of the path of a URL, even URL-encodedfile- used for strings that could contain a pathcookie- used for strings that could contain a http cookieheader- used for strings that could contain a http headersleep- used for values that could set how long the code waits (sleep(), curl timeouts, ...). Unlike the other taints,intandfloatvalues carry itllm_prompt- used for strings that could contain instructions passed to an LLM prompt (prompt injection)user_secret- used for strings that could contain user-supplied secretssystem_secret- used for strings that could contain system secrets
Every taint above except user_secret and system_secret is a kind of user input: the default taint sources give all of them, and a value of a type that can't hold user input (a literal string, null, and a number or boolean except for sleep) carries none of them. Annotations can name them all at once:
input(or its aliastainted) - all the user input taintsinput_except_sleep- all the user input taints exceptsleep
You're also free to define your own taint types when defining custom taint sources – they're just strings.
Taint Sources
Psalm defines the following default taint sources:
- the
$_GET,$_POST,$_COOKIEand$_REQUESTserver variables; - the entries of
$_SERVERthe client sends: the request headers (HTTP_*), the URI and what is taken from it (REQUEST_URI,QUERY_STRING,PATH_INFO,PHP_SELF,argv, ...),REQUEST_METHOD,CONTENT_TYPEand the HTTP authentication credentials, the same entries of a redirected request (REDIRECT_*), and any entry read with a key that is not a literal; - the names, types and full paths of the uploaded files in
$_FILES; - reading from the input stream:
fopen(),file_get_contents()andfile()called with a literalphp://inputorphp://stdinpath, and the stream reading functions (fgets(),fread(),stream_get_contents(), ...) applied to such a handle; - the predefined
STDINconstant (thephp://stdinstream). - reading from the network:
socket_read(), the datasocket_recv(),socket_recvfrom()andsocket_recvmsg()write to their by-reference parameter,stream_socket_recvfrom(),curl_exec()andcurl_multi_getcontent(), and the streams opened withfsockopen(),pfsockopen(),stream_socket_client(),stream_socket_accept()andsocket_export_stream()(so reading from them withfgets(),fread(), ... is a source too). - DNS answers, which whoever runs the name servers of a domain chooses:
dns_get_record(), the hostsgetmxrr()writes to its by-reference parameter, andgethostbyaddr(). - the command-line options
getopt()returns, which whoever runs the script chooses. - exceptions, since the analysis does not follow them from where they are thrown to where they are caught: their message (
getMessage()), their trace (getTraceAsString()) and their string form, and the variable of acatchblock, so dumping a caught exception (print_r(),var_export()) is a source too.
You can also define your own taint sources.
Taint Sinks
Psalm currently defines a number of different sinks for builtin functions and methods, including echo, include, header.
What is written to a stream opened on a literal php://output path, which is the response, or php://stdout, which is the response under the CGI SAPI, is a sink as what echo outputs is: file_put_contents() to such a path, and fwrite(), fputs(), fputcsv(), fprintf(), vfprintf(), stream_copy_to_stream() and the fwrite() and fputcsv() methods of SplFileObject on a stream opened with fopen() or new SplFileObject() on such a path in the same function, and held by its variables.
You can also define your own taint sinks.
Avoiding False-Positives
Nobody likes to wade through a ton of false-positives – here’s a guide to avoiding them.
Limitations
Taint Analysis relies on not making any mistakes when escaping values, e.g.
$sql = 'SELECT * FROM users WHERE id = ' . $mysqli->real_escape_string((string) $_GET['id']);
$html = "
<img src=" . htmlentities((string) $_GET['img']) . " alt='' />
<a href='" . htmlentities((string) $_GET['a1']) . "'>Link 1</a>
<a href='" . htmlentities((string) $_GET['a2']) . "'>Line 2</a>";
// Details:
// $id = 'id' - Missing quotes
// $img = '/ onerror=alert(1)' - Missing quotes
// $a1 = 'javascript:alert(1)' - Normal inline JavaScript
// $a2 = '/' onerror='alert(1)' - Pre PHP 8.1, single quotes are not escaped by default
// Test:
// /?id=id&img=%2F+onerror%3Dalert%281%29&a1=javascript%3Aalert%281%29&a2=%2F%27+onerror%3D%27alert%281%29
To avoid these issues, use Parameterised Queries for SQL and Commands (e.g. exec); and a context-aware templating engine for HTML. Then use the literal-string type to ensure sensitive strings are defined in your application (i.e. have been written by a developer).
Security Issues
The issues of security analysis (TaintedHtml, TaintedSql, ...) flag potential vulnerabilities rather than type errors or code-quality problems. To tell them apart, Psalm's human-readable reports (console, compact, table, by-issue-level and phpstorm) show them under a SECURITY header instead of ERROR:
SECURITY: TaintedHtml - src/index.php:2:1 - Detected tainted HTML
A security issue configured with errorLevel="info" is still shown as INFO. Machine-readable reports (JSON, checkstyle, SARIF, ...) keep their usual severities; the JSON and XML reports also include an is_security field.
Plugins can mark their own issues the same way by implementing the Psalm\Issue\SecurityIssue interface.
Using Baseline With Taint Analysis
Since taint analysis is performed separately from other static code analysis, it makes sense to use a separate baseline for it.
You can use --use-baseline=PATH option to set a different baseline for taint analysis.
Viewing Results in a User Interface
Psalm supports the SARIF standard for exchanging static analysis results. This enables you to view the results in any SARIF compatible software, including the taint flow.
GitHub Code Scanning
GitHub code scanning can be set up by using the Psalm Security Scan action, or the Psalm GitHub Action with report_file: results.sarif (see GitHub Actions).
Alternatively, the generated SARIF file can be manually uploaded as described in the GitHub documentation.
The results will then be available in the "Security" tab of your repository.
Other SARIF compatible software
To generate a SARIF report run Psalm with the --report flag and a .sarif extension. For example:
psalm --report=results.sarif
Debugging the taint graph
Psalm can output the taint graph using the DOT language. This is useful when expected taints are not detected. To generate a DOT graph run Psalm with the --dump-taint-graph flag. For example:
psalm --taint-analysis --dump-taint-graph=taints.dot
dot -Tsvg -o taints.svg taints.dot