# Stale approval trust across changed context

Approval or freshness checks keyed to user or optional parameter instead of the exact target version

_Area: Web business logic and race conditions. Seen in 2 disclosed reports; one confirmed bounty: $150; latest 2026-09-17. Updated 2026-11-01._

## How it works
An approval is meant to vouch for one specific thing: this PR, this commit, this file version. The bug is that the code records or checks the approval against a broader or narrower key than the thing being approved. In Gitea, one approval of a fork PR is stored as "this user is approved" and later checked with a count that only filters repo and user, so it silently covers every future PR, ref and commit [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-777r-4v59-6486.md). In Nextcloud, the freshness check compares the reviewed file's etag to the current one only when the client sends an etag; omit it and the check is skipped, so an approver signs off on content changed after review [E3](https://bugbountylibrary.com/r/h1%3A3610332.md). The broken assumption in both: that the approval's context is fixed, or that the client will supply the field the check depends on.

## Where it shows up
- Approval/trust gates implemented as a DB `Count` or existence query; read the WHERE clause and ask what unit of trust it actually scopes [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-777r-4v59-6486.md).
- Comments or docs stating "first time" / "per PR" intent that the query does not enforce [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-777r-4v59-6486.md).
- Security checks guarded by `if param != ""` or `if param present` on a client-supplied field (etag, version, hash) [E3](https://bugbountylibrary.com/r/h1%3A3610332.md).
- Approval state that persists across later changes to the target (new commits, new content) without re-validation [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-777r-4v59-6486.md)[E3](https://bugbountylibrary.com/r/h1%3A3610332.md).

## How to review
Identify the exact object the approval is supposed to cover (PR id + head SHA, file id + etag). Then confirm the check binds to that object on every use, not to the user, repo, or a parameter the caller controls. Invariant: an approval is valid only for the target and version it was granted against; any change to target, ref, commit, or content invalidates it. For optional fields, absence must fail closed (reject or force re-review), never skip the check [E3](https://bugbountylibrary.com/r/h1%3A3610332.md).

## How finders spotted it
- Read `ifNeedApproval` and compared the comment ("first time user triggered actions") against the actual query, which lacked any PR/commit predicate; confirmed with a live repro [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-777r-4v59-6486.md).
- Noticed the UI sends an etag to detect post-review edits, hypothesized the server check was conditional on its presence, and tested by deleting the parameter [E3](https://bugbountylibrary.com/r/h1%3A3610332.md).

## Variants seen
- User-scoped permanent trust replacing per-PR approval [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-777r-4v59-6486.md).
- Optional-parameter freshness check that fails open when omitted [E3](https://bugbountylibrary.com/r/h1%3A3610332.md).

## Impact and severity
E1 is severe: arbitrary workflow YAML on the runner, and with poisoned caches/artifacts restored by trusted base-branch runs, escape from the fork sandbox with full secret access and internal pivoting [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-777r-4v59-6486.md). E3 lets unreviewed changes be approved, defeating the review control [E3](https://bugbountylibrary.com/r/h1%3A3610332.md). Impact rises with what the approval unlocks (secrets, network reach) and how long the trust persists [E1](https://bugbountylibrary.com/r/ghsa%3AGHSA-777r-4v59-6486.md).

## Evidence
- E1: https://bugbountylibrary.com/r/ghsa%3AGHSA-777r-4v59-6486.md
- E2: https://bugbountylibrary.com/r/h1%3A953083.md
- E3: https://bugbountylibrary.com/r/h1%3A3610332.md
