Skip to content

merge-guard read floor: curl/wget ref-mutation gate keys on literal "api" substring → non-api-host bypass #1061

Description

@michael-wojcik

Summary

The merge-guard read floor (is_dangerous_command in pact-plugin/hooks/shared/merge_guard_common.py, called by merge_guard_pre.py) gates curl/wget GitHub-API ref-mutation commands using patterns that require the literal substring api in the command. A ref-deleting curl/wget to a host whose URL does not contain api therefore evaluates is_dangerous = False and is not gated.

curl -X DELETE <self-hosted-or-$VAR-host>/.../git/refs/heads/y   →  is_dangerous = False   (BYPASS)

The canonical forms always contain api and remain correctly gated:

  • gh api ... (literal api)
  • https://api.github.com/...
  • GHE https://<host>/api/v3/...

The gap is curl/wget to a non-api host (a self-hosted endpoint, an IP, or a $VAR-interpolated host) carrying a mutating method on a git/refs/... path.

Severity

MEDIUM. A real ref-deletion (branch/tag delete) bypass of the read floor. Not reachable via the canonical gh api / api.github.com forms (always contain api); requires a non-api host URL.

Scope / provenance

Fix direction

Gate the read-side curl/wget API ref-mutation form structurally rather than on the literal api substring:

  • HTTP tool present (curl / wget / http/https clients), AND
  • a git/refs/... path fragment, AND
  • a mutating method (-X|--request|--method in {DELETE, POST, PATCH, PUT}, or an implicit-write data flag).

This mirrors the structural test already used for the gh api form and removes the host-substring dependency. Apply symmetrically so mint and read stay equal.

Acceptance

  • curl -X DELETE <non-api-host>/.../git/refs/heads/yis_dangerous = True (gated).
  • All currently-gated api-bearing forms stay gated (no regression).
  • curl -X GET .../git/refs/... (read) stays ungated.

Anti-drift: mint==read parity canary (added scope)

When the fix tightens the read floor so the non-api curl/wget ref-mutation form evaluates is_dangerous = True, the mint side needs no changedetect_command_operation_type already classifies it, so the mint will gate-pass and correctly mint a branch-delete token (mint == read, both gated). The risk is silent drift between the read-side patterns and the mint classifier over time.

Add a mint==read parity canary to the fix's test set: assert that the read floor (is_dangerous_command) and the mint classifier (detect_command_operation_type) agree on the non-api curl/wget ref-mutation form — so a later edit to one cannot diverge from the other. This is the same anti-drift discipline the GAP1 is_dangerous-gate SSOT enforces (one predicate, both consumers).

(Reproduced + corroborated by the security-engineer and architect during PR #1060 v3 verification: under the current is_dangerous-gate, mint == read holds — non-api curl → is_dangerous=False → no mint — so this is purely a read-floor coverage task, not a mint regression.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions