Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
[![Test](https://github.com/be-framework/psalm-plugin/actions/workflows/test.yml/badge.svg)](https://github.com/be-framework/psalm-plugin/actions/workflows/test.yml)

Psalm plugin that detects [Be Framework](https://github.com/be-framework/Be.Framework) runtime errors at static-analysis time.
It also teaches Psalm taint analysis that constructor parameters annotated with
`Ray\InputQuery\Attribute\Input` are user-controlled input on configured root input classes.

## What it detects

Expand Down Expand Up @@ -48,6 +50,52 @@ public function validate(string $value): void

Variable throws and ternary/match unions are resolved via Psalm's `NodeTypeProvider`. When the type cannot be resolved, the plugin stays silent (false-positive avoidance).

### `#[Input]` taint sources

When Psalm is run with `--taint-analysis`, only constructor parameters annotated with
`Ray\InputQuery\Attribute\Input` on configured root input classes are treated as
user-controlled input. `#[Inject]` parameters and unrelated variables are not
tainted. Downstream input classes are not re-tainted after sanitization unless
they are explicitly configured as sources.

Configure the first input classes in `psalm.xml`:

```xml
<plugins>
<pluginClass class="Be\PsalmPlugin\Plugin">
<inputTaintSources>
<class name="App\Input\ProfileInput" />
</inputTaintSources>
</pluginClass>
</plugins>
```

```php
final readonly class ProfileInput
{
public function __construct(
#[Input] public string $name,
) {}

public function render(): void
{
echo $this->name; // reported by Psalm as TaintedHtml
}
}
```

Taint analysis is enabled separately from normal Psalm analysis:

```bash
vendor/bin/psalm --taint-analysis
```

There is a runnable demo in [`demo/`](demo/):

```bash
vendor/bin/psalm --config=demo/psalm.xml --taint-analysis --no-cache --no-progress
```

## Installation

```bash
Expand Down
67 changes: 67 additions & 0 deletions demo/InputTaintDemo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace Be\PsalmPlugin\Demo;

use Ray\Di\Di\Inject;
use Ray\InputQuery\Attribute\Input;

use function htmlspecialchars;

use const ENT_QUOTES;
use const ENT_SUBSTITUTE;

interface TemplateRendererInterface
{
public function renderTrusted(string $template): string;
}

final readonly class GreetingInput
{
public function __construct(
#[Input]
public string $name,
#[Inject]
public TemplateRendererInterface $renderer,
#[Inject]
public string $trustedTemplate,
) {
}

public function unsafeOutput(): void
{
echo $this->name; // TaintedHtml: #[Input] is user-controlled.
}

public function sanitizedInput(): SanitizedGreetingInput
{
return new SanitizedGreetingInput(
htmlspecialchars($this->name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
);
}

public function injectedOutput(): void
{
echo $this->trustedTemplate; // No taint: #[Inject] is not a user input source.
}

public function renderedInjectedOutput(): void
{
echo $this->renderer->renderTrusted($this->trustedTemplate);
}
}

final readonly class SanitizedGreetingInput
{
public function __construct(
#[Input]
public string $name,
) {
}

public function output(): void
{
echo $this->name; // No taint: this downstream Input class is not a source.
}
}
19 changes: 19 additions & 0 deletions demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# `#[Input]` Taint Demo

This demo shows that the plugin treats only configured root input classes as
Psalm taint sources. Downstream input classes are not re-tainted after
sanitization.

Run it from the repository root:

```bash
vendor/bin/psalm --config=demo/psalm.xml --taint-analysis --no-cache --no-progress
```

Expected result:

- Psalm exits non-zero because the unsafe output is intentional.
- `GreetingInput::unsafeOutput()` reports `TaintedHtml` and `TaintedTextWithQuotes` for `echo $this->name`.
- `GreetingInput::sanitizedInput()` returns a downstream input object with an escaped value.
- `SanitizedGreetingInput::output()` is accepted because the downstream input class is not configured as a source.
- `#[Inject]` values are not tainted.
30 changes: 30 additions & 0 deletions demo/psalm.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0"?>
<psalm
errorLevel="1"
phpVersion="8.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config ../vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="." />
<ignoreFiles>
<directory name="../vendor" />
</ignoreFiles>
</projectFiles>

<plugins>
<pluginClass class="Be\PsalmPlugin\Plugin">
<inputTaintSources>
<class name="Be\PsalmPlugin\Demo\GreetingInput" />
</inputTaintSources>
</pluginClass>
</plugins>

<issueHandlers>
<PossiblyUnusedMethod errorLevel="suppress" />
<UnusedClass errorLevel="suppress" />
<UndefinedAttributeClass errorLevel="suppress" />
<UndefinedClass errorLevel="suppress" />
</issueHandlers>
</psalm>
Loading
Loading