Skip to content

POST /state accepts an unlimited number of branchPatterns, so one request can pin a CPU for minutes and keeps running after the client disconnects #116

Closed

Description

@matt-edmondson

What's wrong

BranchStateHandler.TryParsePatterns (GitBranchStateCache/Endpoints/BranchStateHandler.cs, at 61cc96a) caps each pattern's length at BranchPattern.MaximumLength, which is 512. It never caps how many patterns a request may send.

paths does have a limit: MaxPathsPerRequest, enforced at L149-154 with a 400. branchPatterns has nothing equivalent, and the service doesn't lower Kestrel's 30 MB default MaxRequestBodySize.

The patterns are then evaluated by RefResolver.Match (Refs/RefResolver.cs:100-115). That is a synchronous branches × patterns scan with no cancellation token, so it keeps running after the caller hangs up.

Failure scenario

An admitted caller sends one POST /state with about 58,000 patterns of the form "*" + 510×"a" + "b". This needs only read access to one allow-listed repository, and a buggy client could do it by accident. The request fits in the default 30 MB body limit.

Measured with the repository's own BranchPattern: one such pattern against 1,000 60-character branch names takes about 2 ms. One request therefore costs about 116 CPU-seconds per 1,000 mirror branches. It runs on a request thread and can't be cancelled, so a handful of concurrent requests starve every other client's 30-second heartbeat.

GET /branches?pattern= is bounded in practice by Kestrel's request-line limit, so POST /state is the open path.

This is a different code path from #111, which covers the allow-list regex timing out on a long path.

Suggested fix

  • Add MaxBranchPatternsPerRequest to GitBranchStateCacheOptions, defaulting to something like 256, and validate it in GitBranchStateCacheOptionsValidator.
  • Reject a request over the limit with a 400 in TryParsePatterns, so both /state and /branches are covered. Mirror the wording of the paths message.
  • Optionally pass the request's CancellationToken into RefResolver.Match and check it between branches.

Acceptance criteria

  • A /state request with more patterns than the configured limit returns 400 before any mirror fetch or ref listing.
  • The new option is validated as greater than zero and documented next to MaxPathsPerRequest.

Activity

  1. matt-edmondson commented on Nov 3, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    Next steps: Add MaxBranchPatternsPerRequest (default around 256, validated > 0) and return a 400 in TryParsePatterns before any mirror fetch, so /state and /branches are both covered. Threading the request CancellationToken through RefResolver.Match is a worthwhile follow-on, but not required to close this.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions