Custom Taint Sinks

The @psalm-taint-sink <taint-type> <param-name> annotation allows you to define a taint sink.

Any tainted value matching the given taint type will be reported as an error by Psalm.

Example

Here the PDOWrapper class has an exec method that should not receive tainted SQL, so we can prevent its insertion:

<?php

class PDOWrapper {
    /**
     * @psalm-taint-sink sql $sql
     */
    public function exec(string $sql) : void {}
}

Values of an array

Append [*] to the parameter name to make only the values of the array given to the parameter a sink, not its keys:

<?php

final class TemplateLoader {
    /**
     * @param array<string, string> $templates template names => template sources
     * @psalm-taint-sink eval $templates[*]
     */
    public function __construct(private array $templates) {}
}

Here a tainted template name is not reported, a tainted template source is.

Conditional sinks

Like conditional escapes, the taint type can be a conditional type on the parameters. Each call resolves it with its arguments:

<?php

final class Renderer {
    public const ESCAPED = 0;
    public const RAW = 1;

    /**
     * @param array{mode?: self::ESCAPED|self::RAW, ...} $options
     * @psalm-taint-sink ($options is array{mode?: self::ESCAPED, ...} ? null : 'html') $content
     */
    public function render(string $content, array $options = []): void {}
}

Here render($text) and render($text, ['mode' => Renderer::ESCAPED]) aren't sinks, while render($text, ['mode' => Renderer::RAW]) is an HTML sink. When the mode is only known at runtime, both branches apply.