> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pipefort.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SLSA-BUILD-L3 — Cache key in pull_request_target derived from PR input

> An attacker can poison the cache for the base branch when keys come from PR-controlled context.

| Field      | Value                                                          |
| ---------- | -------------------------------------------------------------- |
| Rule ID    | `slsa-build-l3-cache-poisoning`                                |
| Severity   | **HIGH**                                                       |
| SLSA level | [v1.2 Build L3](https://slsa.dev/spec/v1.2/build-track-basics) |
| Auto-fix   | ✗                                                              |

## What the check does

Fires when **all** of the following are true:

1. The workflow triggers on `pull_request_target`.
2. A job has an `actions/cache@…` step.
3. That step's `key:` (or `restore-keys:`) interpolates a PR-controlled
   context: `github.head_ref`, `github.event.pull_request.head.{ref,sha,label}`,
   or `github.event.pull_request.{title,body,number}`.

## Why it matters

`pull_request_target` runs in the **base branch's privileged context** with
repository secrets and write tokens. If the cache key is derived from data the
attacker controls (a PR's head ref, title, etc.), the attacker can plant a
cache entry that subsequent trusted base-branch builds will restore — letting
them inject arbitrary files into the build, violating SLSA Build L3's
isolation guarantee.

## Vulnerable example

```yaml theme={null}
on: pull_request_target
jobs:
  cache-deps:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/cache@<sha>
        with:
          path: ~/.npm
          key: ${{ runner.os }}-${{ github.head_ref }}     # ← PR-controlled
```

## Safe alternatives

```yaml theme={null}
# Option 1: derive the key from trusted state only.
- uses: actions/cache@<sha>
  with:
    path: ~/.npm
    key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

# Option 2: move PR-specific caching to a non-elevated pull_request workflow.
on: pull_request                    # ← no secrets, no write permissions
```
