GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Psalm
  • GitHub
  • Psalm docs
  • Installation
  • Configuration
  • Using plugins
  • Authoring plugins
  • How Psalm represents types
  • Command line usage
  • IDE support
  • Dealing with code issues
  • Overview
  • AbstractInstantiation
  • AbstractMethodCall

This view

  • Original page
  • New address

GeezerVison is showing psalm.dev — Security Analysis in Psalm - Psalm



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 SQL
  • nosql - 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 to string, int, float or bool removes it
  • ldap - used for strings that could contain a ldap DN or filter
  • xpath - used for strings that could contain an XPath expression
  • html - used for strings that could contain angle brackets or unquoted strings
  • has_quotes - used for strings that could contain unquoted strings
  • shell - used for strings that could contain shell commands
  • callable - used for callable strings that could be user-controlled
  • unserialize - used for strings that could contain a serialized string
  • include - used for strings that could contain a path being included
  • eval - used for strings that could contain code
  • extract - used for arrays that could be passed to extract(), overwriting variables
  • ssrf - used for strings that could contain text passed to Curl or similar
  • url_component - used for strings that could contain URL syntax (/, ?, &, #...) placed in the path, query or fragment of a URL
  • url_path - used for strings that could be a . or .. segment of the path of a URL, even URL-encoded
  • file - used for strings that could contain a path
  • cookie - used for strings that could contain a http cookie
  • header - used for strings that could contain a http header
  • sleep - used for values that could set how long the code waits (sleep(), curl timeouts, ...). Unlike the other taints, int and float values carry it
  • llm_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 secrets
  • system_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 alias tainted) - all the user input taints
  • input_except_sleep - all the user input taints except sleep

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, $_COOKIE and $_REQUEST server variables;
  • the entries of $_SERVER the 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_TYPE and 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() and file() called with a literal php://input or php://stdin path, and the stream reading functions (fgets(), fread(), stream_get_contents(), ...) applied to such a handle;
  • the predefined STDIN constant (the php://stdin stream).
  • reading from the network: socket_read(), the data socket_recv(), socket_recvfrom() and socket_recvmsg() write to their by-reference parameter, stream_socket_recvfrom(), curl_exec() and curl_multi_getcontent(), and the streams opened with fsockopen(), pfsockopen(), stream_socket_client(), stream_socket_accept() and socket_export_stream() (so reading from them with fgets(), fread(), ... is a source too).
  • DNS answers, which whoever runs the name servers of a domain chooses: dns_get_record(), the hosts getmxrr() writes to its by-reference parameter, and gethostbyaddr().
  • 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 a catch block, 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
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.