{
  "parent_claim": {
    "cve": "CVE-2026-85706",
    "entrypoint": "POST /api/v4/projects/:id/repository/commits.json (format-suffix defeat of workhorse route classification)",
    "endpoint_file": "lib/api/commits.rb",
    "sink": "API::Helpers::CommitsBodyUploaderHelper#file_params_from_body_upload -> File.exist?/File.read(params['file.path']) -> Rack::Utils.parse_nested_query(file content) -> InvalidParameterError message echoed in 400 response"
  },
  "variant_claim": {
    "entrypoints": [
      "POST /api/v4/projects/:id/repository/files/<segment>/ (trailing slash)",
      "PUT /api/v4/projects/:id/repository/files/<segment>/ (trailing slash)"
    ],
    "endpoint_file": "lib/api/files.rb (post/put ':id/repository/files/:file_path', lines 362/407 in 19.3.1)",
    "sink": "identical: API::Helpers::CommitsBodyUploaderHelper#file_params_from_body_upload -> File.exist?/File.read(params['file.path']) -> Rack::Utils.parse_nested_query(file content) -> InvalidParameterError message echoed in 400 response"
  },
  "shared_root_cause": [
    "1. Workhorse classifies body-upload-accelerated API routes with \\z-anchored regexes matched on the clean (escaped) request path; a request whose clean path does not match (format suffix or trailing slash appended) is proxied raw to Rails with workhorse's internal header, skipping upload finalization.",
    "2. require_gitlab_workhorse! only asserts the request transited workhorse (true for all omnibus traffic); it is not an authentication check.",
    "3. In 19.3.1, commits.rb post ':id/repository/commits' AND files.rb post/put ':id/repository/files/:file_path' lack authenticate! and both call file_params_from_body_upload, which treats the flat raw request parameter file.path as a filesystem path.",
    "4. Rails/Grape routing tolerates what workhorse's regexes reject: it strips an optional (.:format) suffix and routes trailing-slash paths, so the two layers disagree about the request's identity."
  ],
  "differences_from_parent": [
    "Different Grape endpoint (Repository Files API create/update-file instead of Repository Commits API create-commit).",
    "Different classification-defeat encoding: trailing slash instead of .json suffix - necessary because the files route regex tail is a wildcard segment ([^/]+) that any single-segment suffix still matches; only a clean path ending in '/' (or containing a second tail segment) fails the regex while still routing.",
    "Additional HTTP method surface (PUT as well as POST).",
    "Same flat parameter names (file, file.size, Content-Type, file.path) and same content-echo oracle."
  ],
  "equivalence_evidence": {
    "same_sink_runtime": "Both entry points echo the identical canary token PRUVA85706_VCANARY_VC1_d41d8cd98f00_PCTBYTE_%zz_END in the same 400 'Invalid parameter: invalid %-encoding (...)' response shape on 19.3.1 (vuln_t1/t2 vs vuln_t3/t4 artifacts).",
    "same_mechanism_control": "The plain (non-trailing-slash, non-suffixed) files path is intercepted by workhorse and does NOT read the attacker path (400 'branch is required'), proving the file read depends on defeating workhorse classification - the same operative mechanism as the parent PoC's no-suffix 401/intercepted control.",
    "same_fix_covers_both": "The v19.3.2 fix (authenticate! added to commits.rb, files.rb and workhorse_authorize_commits_body_upload!; ::UploadedFile-only sink) returns 401 for both the parent entry point and the alternate trigger on 19.3.2-ce.0.",
    "workhorse_unchanged": "Route regexes extracted from the shipped gitlab-workhorse binaries are identical between 19.3.1-ce.0 and 19.3.2-ce.0 (bundle/logs/vuln_variant/workhorse_regexes.txt), so the fix defends the Rails handlers rather than the classification layer."
  },
  "same_root_cause_confidence": "high",
  "same_surface_confidence": "high"
}
