Skip to content
1 change: 1 addition & 0 deletions config.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@
<xs:element name="TaintedInclude" type="IssueHandlerType" minOccurs="0" />
<xs:element name="TaintedInput" type="IssueHandlerType" minOccurs="0" />
<xs:element name="TaintedLdap" type="IssueHandlerType" minOccurs="0" />
<xs:element name="TaintedLlmPrompt" type="IssueHandlerType" minOccurs="0" />
<xs:element name="TaintedShell" type="IssueHandlerType" minOccurs="0" />
<xs:element name="TaintedSleep" type="IssueHandlerType" minOccurs="0" />
<xs:element name="TaintedSql" type="IssueHandlerType" minOccurs="0" />
Expand Down
1 change: 1 addition & 0 deletions docs/running_psalm/issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@
- [TaintedInclude](issues/TaintedInclude.md)
- [TaintedInput](issues/TaintedInput.md)
- [TaintedLdap](issues/TaintedLdap.md)
- [TaintedLlmPrompt](issues/TaintedLlmPrompt.md)
- [TaintedShell](issues/TaintedShell.md)
- [TaintedSleep](issues/TaintedSleep.md)
- [TaintedSql](issues/TaintedSql.md)
Expand Down
17 changes: 17 additions & 0 deletions docs/running_psalm/issues/TaintedLlmPrompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# TaintedLlmPrompt

Emitted when user-controlled input can be passed into an LLM prompt, risking prompt injection.

```php
<?php

class LlmAgent {
/** @psalm-taint-sink llm_prompt $prompt */
public function prompt(string $prompt): string {
return "";
}
}

$agent = new LlmAgent();
$agent->prompt((string) $_GET["question"]);
```
7 changes: 7 additions & 0 deletions src/Psalm/Internal/Codebase/TaintFlowGraph.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Psalm\Issue\TaintedHtml;
use Psalm\Issue\TaintedInclude;
use Psalm\Issue\TaintedLdap;
use Psalm\Issue\TaintedLlmPrompt;
use Psalm\Issue\TaintedSSRF;
use Psalm\Issue\TaintedShell;
use Psalm\Issue\TaintedSleep;
Expand Down Expand Up @@ -612,6 +613,12 @@ private function getChildNodes(
$issue_trace,
$path,
),
TaintKind::INPUT_LLM_PROMPT => new TaintedLlmPrompt(
'Detected tainted LLM prompt',
$issue_location,
$issue_trace,
$path,
),
default => new TaintedCustom(
'Detected tainted ' . $codebase->custom_taints[$t],
$issue_location,
Expand Down
10 changes: 10 additions & 0 deletions src/Psalm/Issue/TaintedLlmPrompt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Psalm\Issue;

final class TaintedLlmPrompt extends TaintedInput
{
public const SHORTCODE = 367;
}
17 changes: 13 additions & 4 deletions src/Psalm/Type/TaintKind.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,26 @@ final class TaintKind
public const INPUT_XPATH = (1 << 13);
public const INPUT_SLEEP = (1 << 14);
public const INPUT_EXTRACT = (1 << 15);
public const USER_SECRET = (1 << 16);
public const SYSTEM_SECRET = (1 << 17);
public const INPUT_LLM_PROMPT = (1 << 16);
public const USER_SECRET = (1 << 17);
public const SYSTEM_SECRET = (1 << 18);

public const ALL_INPUT = (1 << 16) - 1;
/**
* Bitmask of all INPUT_* taint types. Used as the default taint for
* user-controlled sources (e.g. $_GET, $_POST) so they propagate to any
* input sink.
* Excludes USER_SECRET and SYSTEM_SECRET, which represent
* sensitive data leaking out rather than untrusted data flowing in.
*/
public const ALL_INPUT = (1 << 17) - 1;

/** @internal */
public const NUMERIC_ONLY = self::INPUT_SLEEP;
/** @internal */
public const BOOL_ONLY = self::INPUT_SLEEP;

/** @internal Keep this synced with the above */
public const BUILTIN_TAINT_COUNT = 18;
public const BUILTIN_TAINT_COUNT = 19;


// Map of taint kind names to their bitmask values, used in taint annotations
Expand All @@ -64,6 +72,7 @@ final class TaintKind
'extract' => self::INPUT_EXTRACT,
'user_secret' => self::USER_SECRET,
'system_secret' => self::SYSTEM_SECRET,
'llm_prompt' => self::INPUT_LLM_PROMPT,

'input_except_sleep' => self::ALL_INPUT & ~self::INPUT_SLEEP,

Expand Down
74 changes: 74 additions & 0 deletions tests/TaintTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,37 @@ class PDOWrapper {
public function exec(string $sql) : void {}
}',
],
'llmPromptWithSafeInput' => [
'code' => '<?php
class LlmAgent {
/**
* @psalm-taint-sink llm_prompt $prompt
*/
public function prompt(string $prompt): string {
return "";
}
}

$agent = new LlmAgent();
$agent->prompt("Summarize this document");',
],
'llmPromptEscaped' => [
'code' => '<?php
class LlmAgent {
/** @psalm-taint-sink llm_prompt $prompt */
public function prompt(string $prompt): string {
return "";
}
}

/** @psalm-taint-escape llm_prompt */
function sanitize_for_llm(string $input): string {
return $input;
}

$agent = new LlmAgent();
$agent->prompt(sanitize_for_llm((string) $_GET["question"]));',
],
'taintedInputToParamButSafe' => [
'code' => '<?php
class A {
Expand Down Expand Up @@ -815,6 +846,49 @@ function my_escaping_function_for_seconds(mixed $input) : int {};
public function providerInvalidCodeParse(): array
{
return [
'taintedLlmPromptFromUserInput' => [
'code' => '<?php
class LlmAgent {
/** @psalm-taint-sink llm_prompt $prompt */
public function prompt(string $prompt): string {
return "";
}
}

$agent = new LlmAgent();
$agent->prompt((string) $_GET["question"]);',
'error_message' => 'TaintedLlmPrompt',
],
'taintedLlmPromptFromConcatenatedInput' => [
'code' => '<?php
class LlmAgent {
/** @psalm-taint-sink llm_prompt $prompt */
public function prompt(string $prompt): string {
return "";
}
}

$agent = new LlmAgent();
$agent->prompt("Tell me about " . (string) $_GET["topic"]);',
'error_message' => 'TaintedLlmPrompt',
],
'taintedLlmPromptThroughFunction' => [
'code' => '<?php
class LlmAgent {
/** @psalm-taint-sink llm_prompt $prompt */
public function prompt(string $prompt): string {
return "";
}
}

function buildPrompt(string $userInput): string {
return "Tell me about " . $userInput;
}

$agent = new LlmAgent();
$agent->prompt(buildPrompt((string) $_GET["topic"]));',
'error_message' => 'TaintedLlmPrompt',
],
'taintedInputFromMethodReturnTypeSimple' => [
'code' => '<?php
class A {
Expand Down
Loading