Ctrl K
ComunidadGuías…

patches/README.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Reference patches — `patches/<target>/`

A **reference patch** is a minimal, real source fix that closes exactly one
challenge's vulnerability in a target app. It is the input to the
**positive-direction** scoring gate (`scripts/acceptance-patched.sh`): proof that a
*correctly patched* contestant fork scores the points it should — the mirror of
`scripts/acceptance-target.sh` (`stock-scores-zero`), which only proves an
*unpatched* app scores nothing.

## Why this exists

The kit scores a contestant by running executable `node:test` challenge suites
against their app. Each suite *exploits* a vulnerability and asserts the exploit
does **not** succeed, so a challenge scores as "patched" only when the vuln is
genuinely closed. Nothing else in the kit proves the *positive* direction — a
scorer that quietly stopped awarding points for a real fix would pass every other
gate. These patches close that gap.

## The convention

```
patches/<target>/<challenge-id>.patch
```

- `<challenge-id>` is the lowercased catalogue key
  (`Challenge-3-SQLi` → `challenge-3-sqli`), the same id the scorer reports and
  stores.
- Each file is a **`git`-format unified diff** rooted at the app source tree. The
  harness applies it with `git apply`.
- A patch fixes the vulnerability in **source**, such that the exploit fails. The
  fix must be **surgical**: it closes its one challenge and leaves the app
  otherwise healthy and still vulnerable everywhere else. A patch that breaks the
  build or boot would *also* stop the exploit — and score the challenge "patched"
  for the wrong reason. The harness rejects that (see below).

## Which source tree each patch targets

The patched gate stages a **pinned source tree** and applies the reference patch to
it. `scripts/acceptance-patched.sh` carries a per-target pin (a commit SHA, never a
branch or tag — either can move and silently score a different app):

- **vampi** stages the upstream `erev0s/VAmPI` at the same commit
  `setup/targets.tsv` pins.
- **The other five** stage an `OWASP-CTF/<target>` repo at the script's own SHA.
  Those trees are not the source a contestant forks: `setup/ctf-setup.sh` forks
  each target from the upstream repo and `ref` in `setup/targets.tsv`. The
  per-challenge fixes are extracted from the private rubric repo's
  `solutions/<target>/patch.diff` and apply to the staged tree, so each minimal
  patch is a strict subset of a fix known to score there.

## How the harness uses them

`scripts/acceptance-patched.sh <target> <challenge-id>`:

1. Builds the scorer image, stages the pinned source, applies the patch
   (`git apply --check` first, so a stale patch fails loudly).
2. Builds the fork from its own root `Dockerfile` — the same path a real
   contestant PR takes — and runs the judge over **every** challenge.
3. Asserts the **positive** result: exactly the patched challenge is solved, its
   points equal the catalogue difficulty (parsed straight from the catalogue JSON,
   independent of the scorer's loader), and the other `N-1` challenges still fail.
   `N` is derived from the catalogue, not hardcoded.

### Anti-vacuous discipline

A *real* positive must be told apart from a broken app that merely stopped being
exploitable. Three signals do that:

- The judge writes `ctf-score.md` **only when every challenge was measured**. If
  the app stops answering mid-run the exec runner aborts and writes nothing (see
  `scorer/src/judge.js`, `run.aborted`) — so the report existing at all proves all
  `N` challenges ran. "1/N with the others unreached" cannot occur.
- Exactly one `✅ Patched` and `N-1` `❌ Not yet`. A `❌` means that challenge's
  exploit *ran and succeeded* — the app is up and still genuinely vulnerable there.
- **Positive control.** The `N-1` failing rows prove the app is live for every
  challenge *except* the patched one — so on their own they cannot tell "vuln
  closed" from "endpoint broke" for the `✅` row (an exploit test that only asserts
  the exploit is absent scores `✅` just as well if the patched route now 500s or
  was gutted to return nothing). So the harness boots the patched app and probes
  the endpoint the patch touched with a **benign, valid** request: it must still
  serve real data. A broken/gutted endpoint FAILS the gate.

The control has three strategies, by target shape:

| Strategy     | Targets                               | How the control boots + probes |
| ------------ | ------------------------------------- | ------------------------------ |
| `standalone` | vampi, juice-shop, webgoat, vulnerableapp | boot the single patched image, probe its endpoint |
| `with-db`    | dvwa                                  | boot the app + a MariaDB sibling, init the DB, log in, probe a benign row |
| `exempt`     | securityshepherd                      | see below |

**securityshepherd is control-exempt.** It runs as a three-container Maven/TLS
stack (Tomcat + MariaDB + Mongo) built by the bring-up with no prebuilt image, so
it cannot be re-booted standalone here. Its anti-vacuous guarantee rests on the two
in-judge signals only (report-exists ⇒ all `N` measured; `N-1` genuinely failed ⇒
app live and vulnerable). The one residual gap — a patched row "scored" by breaking
its own endpoint — is not closed by a standalone probe for this target, and is
called out honestly rather than papered over.

## Coverage — honest status

One challenge per target, enough to prove the mechanism (and, across vampi's and
securityshepherd's two each, that the assertion *discriminates* — patching one
scores that one and not another).
This is **not** full per-challenge coverage; the remaining challenges are follow-on
work, each needing its own patch authored and verified against a real build.

| Target            | Challenge id                          | Pts | File patched (fix) | Control | Build-verified |
| ----------------- | ------------------------------------- | :-: | ------------------ | ------- | -------------- |
| vampi             | `challenge-3-sqli`                    |  2  | `models/user_model.py` — parameterize the username lookup | standalone | ✅ |
| vampi             | `challenge-1-excessive-data-exposure` |  1  | `api_views/users.py` — `/_debug` drops password/admin fields | standalone | ✅ |
| juice-shop        | `challenge-1-password-hash-leak`      |  2  | `routes/currentUser.ts` — allow-list the `?fields=` projection | standalone | ✅ |
| dvwa              | `challenge-7-sql-injection-low`       |  1  | `vulnerabilities/sqli/source/low.php` — cast `id` to int | with-db | ✅ |
| webgoat           | `challenge-23-sql-injection-dml-update` | 2 | `…/sqlinjection/introduction/SqlInjectionLesson8.java` — parameterize attack8 | standalone | ✅ |
| vulnerableapp     | `challenge-33-error-sqli-level-1`     |  1  | `…/sqlInjection/ErrorBasedSQLInjectionVulnerability.java` (Level-1) — parameterize + swallow error; **adds a root `Dockerfile`** (fork ships only `Dockerfile.base`) | standalone | ✅ |
| securityshepherd  | `challenge-23-sqli-1`                  |  1  | `…/servlets/module/challenge/SqlInjection1.java` — `PreparedStatement` | exempt | CI-only |
| securityshepherd  | `challenge-10-idor-2`                  |  2  | `…/servlets/module/challenge/DirectObject2.java` — allow-list the five exposed `userId`s; anything else gets the not-found response without a query | exempt | CI-only |

`Build-verified` = the patched fork was actually built and judged (locally and/or
in CI) and the gate passed. securityshepherd's patches are authored and apply-clean
but its multi-container Maven build is not run outside CI.

## Pinning

Each target's source pin lives in `scripts/acceptance-patched.sh` (overridable via
the `*_UPSTREAM_REF` env vars). Bump a pin only together with a fresh run of the
gate and a re-verification (or regeneration) of that target's patch against the new
tree.