{
  "stage": "vuln_variant",
  "parent_root_cause": "In @fastify/middie 9.1.0-9.3.2, lib/engine.js normalizePathForMatching() decodes the percent-encoded slash %2F to a literal '/' (via FindMyWay.sanitizeUrlPath) BEFORE matching middleware regexps, while Fastify's router (find-my-way) preserves %2F during route lookup. The two layers disagree on the canonical request path: for a parameterized guard with a static suffix (e.g. /user/:id/comments), the decoded path /user/a/b/comments has an extra segment and fails to match the guard regexp, so the guard is skipped; the router still matches /user/:id/comments (id='a/b') and dispatches the handler. Result: unauthenticated authz/authn bypass, method-agnostic, no preconditions.",
  "parent_sink": "lib/engine.js :: normalizePathForMatching(url, options) -- the only function that decodes/normalizes the URL for middleware matching (verified single sink via search_code: no other decodeURI/decodeURIComponent/sanitizeUrlPath/safeDecodeURI call site exists in the package). Called once per request from run(), which is invoked from the hook handler registered in index.js (onRequest default, or preValidation/preHandler/etc.).",
  "candidate_variants_tested": [
    {
      "candidate": "single-encoded %2F / %2f / mixed case / multiple %2F / param=%2F",
      "same_root_cause": true,
      "same_sink": true,
      "distinct_entry_or_data_path": false,
      "bypasses_vulnerable": true,
      "bypasses_fixed": false,
      "notes": "Same root cause/sink as parent; trivial encodings of the same slash. Fix's safeDecodeURI preserves all of them."
    },
    {
      "candidate": "double/triple/quad-encoded %252F / %25252F / %2525252F",
      "same_root_cause": "would-be-same",
      "same_sink": true,
      "distinct_entry_or_data_path": true,
      "bypasses_vulnerable": false,
      "bypasses_fixed": false,
      "notes": "Not a bypass vector on EITHER build: the vulnerable sanitizeUrlPath is also single-pass, so %252F decodes once to literal %2F (not '/'), and the guard matches. The fix's decodeNestedPercentEncodedBytes keeps this consistent; it only emits %XX (never '/'), so even the mild middie/router discrepancy it introduces is non-segment-breaking on both sides."
    },
    {
      "candidate": "structural: multi-param guards (/api/:org/:repo) and prefix guard (/files/:dir, end:false)",
      "same_root_cause": true,
      "same_sink": true,
      "distinct_entry_or_data_path": true,
      "bypasses_vulnerable": false,
      "bypasses_fixed": false,
      "notes": "Multi-param with %2F in one param behaves like the parent (guarded on vuln? actually 401 on vuln here because the OTHER param still forces a match... empirically 401 on both). Prefix (end:false) guard is NOT bypassed even on vuln because the decoded extra slash still matches the open-ended prefix -- a scoping nuance of the same root cause, not a distinct variant."
    },
    {
      "candidate": "router-option combinations (ignoreTrailingSlash / ignoreDuplicateSlashes / useSemicolonDelimiter, alone + combined)",
      "same_root_cause": true,
      "same_sink": true,
      "distinct_entry_or_data_path": true,
      "bypasses_vulnerable": true,
      "bypasses_fixed": false,
      "notes": "Same sink; options only add pre/post transforms (removeDuplicateSlashes, trimLastSlash) or share the semicolon-delimiter flag with find-my-way. The fix's safeDecodeURI honors useSemicolonDelimiter identically to the router, so no new discrepancy."
    },
    {
      "candidate": "method-agnosticism (GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)",
      "same_root_cause": true,
      "same_sink": true,
      "distinct_entry_or_data_path": false,
      "bypasses_vulnerable": true,
      "bypasses_fixed": false,
      "notes": "Normalization is in run(), independent of method/hook. The CVE documents the bypass as method-agnostic; this confirms the fix is method-agnostic too. Same surface, not a distinct variant."
    },
    {
      "candidate": "alternate registration entry point: encapsulated prefixed plugin use() (index.js prefix prepend)",
      "same_root_cause": true,
      "same_sink": true,
      "distinct_entry_or_data_path": true,
      "bypasses_vulnerable": true,
      "bypasses_fixed": false,
      "notes": "index.js use() prepends this.prefix (a static string) to the guard path; the compiled guard is /api/user/:id/comments. The same normalizePathForMatching sink processes the full URL, so the fix covers it. A genuinely different registration path, but it converges on the same (fixed) sink."
    }
  ],
  "equivalence_conclusion": "All candidate variants that bypass the vulnerable build share the parent's root cause and the single sink (normalizePathForMatching); they are trivial encodings, option/method permutations, or alternate registration paths that all funnel through the same decoder. The fix replaced that sole sink with find-my-way's safeDecodeURI (preserving %2F) and added a nested-decode helper that cannot recreate a literal slash, so every candidate is blocked on the fixed build. No candidate constitutes a DISTINCT variant that reaches the bug via a path the fix does not cover. Negative variant result: root-cause-equivalent candidates exist but no bypass/distinct-surface variant is confirmed on 9.3.3 (= latest).",
  "same_root_cause_confidence": "high",
  "distinct_variant_confirmed": false
}
