{"id":"CVE-2026-123806","kind":"vulnerability","aliases":["CVE-2026-123806","GHSA-qp9x-wp8f-qgjj"],"related":[],"title":"tuf has platform-dependent delegation path matching","description":"`DelegatedRole._is_target_in_pathpattern` uses `fnmatch.fnmatch` to decide whether a given target path is authorized by a delegation's glob pattern.\n\nPython's `fnmatch.fnmatch` calls `os.path.normcase()` on both arguments before matching. On POSIX hosts `normcase` is the identity function; on Windows hosts `os.path` resolves to `ntpath`, whose `normcase` lowercases its input and replaces `/` with `\\`.\n\nAs a result, python-tuf's delegation *path pattern* matching is case-sensitive on Linux/macOS but case-INSENSITIVE on Windows. This makes the authorization decision for a target dependent on the host operating system of the client running the updater.\n\nThe result on Windows is a TUF specification violation in the python-tuf `ngclient` implementation.\n\n## Vulnerable code\n\n`tuf/api/_payload.py` (HEAD `7ecb67d`):\n\n```python\n1183  @staticmethod\n1184  def _is_target_in_pathpattern(targetpath: str, pathpattern: str) -> bool:\n1185      \"\"\"Determine whether ``targetpath`` matches the ``pathpattern``.\"\"\"\n1186      # We need to make sure that targetpath and pathpattern are pointing to\n1187      # the same directory as fnmatch doesn't threat \"/\" as a special symbol.\n1188      target_parts = targetpath.split(\"/\")\n1189      pattern_parts = pathpattern.split(\"/\")\n1190      if len(target_parts) != len(pattern_parts):\n1191          return False\n1192\n1193      # Every part in the pathpattern could include a glob pattern, that's why\n1194      # each of the target and pathpattern parts should match.\n1195      for target, pattern in zip(target_parts, pattern_parts, strict=True):\n1196          if not fnmatch.fnmatch(target, pattern):\n1197              return False\n1198      return True\n```\n\n`fnmatch.fnmatch` source (Python 3.12, unchanged in current mainline):\n\n```python\ndef fnmatch(name, pat):\n    ...\n    name = os.path.normcase(name)\n    pat = os.path.normcase(pat)\n    return fnmatchcase(name, pat)\n```\n\n## Fix\n\nReplace `fnmatch.fnmatch` with `fnmatch.fnmatchcase`, which is explicitly documented as \"not applying case normalization\", so it behaves identically across platforms.\n\n## Attack\n\n1. A TUF repository with two path-based delegations whose patterns differ only in case — for example, `Foo/*` and `foo/*`.\n2. The \"attacker\" delegation is listed BEFORE the \"legit\" delegation in the delegation order.\n3. The client searches for `foo/something`: on Windows, it will find the \"attacker\" provided target \"Foo/something\".\n\n\n## Exploitability caveats \n\n* The attack needs a repository configuration with case-colliding delegation path patterns. The attacker must control one of the delegated roles.\n* Delegation ordering matters: the attacker-controlled role must be visited BEFORE the legit role in the pre-order walk.\n* The client must run on Windows. No effect on Linux/macOS.\n\n## Credit\n\nReporter: Koda Reef @kodareef5 \nAdvisory edits: Jussi Kukkonen @jku","summary":"On Windows, python-tuf's ngclient matches delegated target paths case-insensitively, while Linux and macOS match case-sensitively, so the same repository can authorize different targets depending on the client's operating system. An attacker who controls a delegated role with a case-differing path pattern placed earlier in the delegation order can get a target accepted on Windows clients.","severity":"medium","cvss":4.0,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"tuf","fixed":["7.0.0"],"affected":"<= 6.0.0","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["cwe-178","ngclient","package","python","tuf","windows"],"not_affected_if":["linux-or-macos-only","no-path-delegations","tuf-7.0.0-or-later"],"how_to_check":"- Check the installed `tuf` version with `pip show tuf` and compare it to the fixed range.\n- Look for Windows hosts or CI runners that run python-tuf `ngclient` to update trusted metadata or targets.\n- Review the repository's delegations for path patterns that differ only in letter case.","fix":"- `tuf` moves to `7.0.0` (affected up to `6.0.0`). The upstream fix replaces `fnmatch.fnmatch` with `fnmatch.fnmatchcase`.","status":"open","published_at":"2026-05-28T22:46:13+00:00","first_seen_at":"2026-11-09T20:23:06+00:00","triaged_at":"2026-11-09T20:24:25+00:00","updated_at":"2026-11-09T20:24:25+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-11-09T20:24:25Z","model":"claude-haiku-5-5","output":{"fix":"- `tuf` moves to `7.0.0` (affected up to `6.0.0`). The upstream fix replaces `fnmatch.fnmatch` with `fnmatch.fnmatchcase`.","reach":"network","title":"python-tuf: delegation path matching ignores case on Windows","impact":"medium","summary":"On Windows, python-tuf's ngclient matches delegated target paths case-insensitively, while Linux and macOS match case-sensitively, so the same repository can authorize different targets depending on the client's operating system. An attacker who controls a delegated role with a case-differing path pattern placed earlier in the delegation order can get a target accepted on Windows clients.","audience":"developers","category":"package","nicknames":[],"who_cares":["python","tuf","windows","ngclient"],"confidence":0.8,"fix_status":"available","needs_auth":"yes","worst_case":"Attacker-supplied target accepted by Windows clients, bypassing delegation rules","analyst_note":"","how_to_check":"- Check the installed `tuf` version with `pip show tuf` and compare it to the fixed range.\n- Look for Windows hosts or CI runners that run python-tuf `ngclient` to update trusted metadata or targets.\n- Review the repository's delegations for path patterns that differ only in letter case.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["linux-or-macos-only","no-path-delegations","tuf-7.0.0-or-later"],"who_is_affected":"Teams that run python-tuf ngclient on Windows hosts to verify TUF repositories with path-based delegations. Exposure needs a repository whose delegation patterns differ only in case and an attacker-controlled delegated role visited first. Linux and macOS clients are not affected, and repositories without path-based delegations can skip it.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"unknown"},"cost_usd":0.000896,"input_tokens":5638,"output_tokens":664,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-123806':11A,19A '-178':22B '-2026':10A,18A '1':367C '1183':209C '1184':211C '1185':222C '1186':229C '1187':242C '1188':255C '1189':259C '1190':263C '1191':271C '1192':274C '1193':275C '1194':289C '1195':299C '1196':311C '1197':317C '1198':320C '2':388C '3':402C '3.12':326C '7ecb67d':207C 'accept':89B 'across':364C 'advisori':478C 'appli':357C 'argument':118C 'attack':66B,366C,390C,414C,421C,434C,447C 'attacker-control':446C 'author':55B,104C,170C 'base':375C 'behav':362C 'bool':221C 'call':114C 'case':40B,48B,75B,157C,163C,358C,382C,428C 'case-collid':427C 'case-diff':74B 'case-insensit':39B,162C 'case-sensit':47B,156C 'caveat':419C 'client':61B,92B,183C,404C,464C 'code':204C 'collid':429C 'configur':425C 'control':68B,436C,448C 'could':281C 'credit':473C 'current':329C 'cve':9A,17A 'cwe':21B 'decid':97C 'decis':171C 'def':212C,332C 'deleg':6A,36B,70B,83B,107C,151C,376C,391C,397C,400C,430C,440C,442C 'delegatedrole._is_target_in_pathpattern':93C 'depend':5A,58B,175C 'determin':223C 'differ':56B,76B,379C 'directori':245C 'document':354C 'doesn':248C 'earlier':80B 'edit':479C 'effect':470C 'everi':276C 'exampl':384C 'explicit':353C 'exploit':418C 'fals':273C,319C 'find':412C 'fix':346C 'fnmatch':247C,333C 'fnmatch.fnmatch':95C,113C,314C,323C,348C 'fnmatch.fnmatchcase':350C 'fnmatchcas':343C 'foo':385C,387C 'foo/something':407C,417C 'function':128C 'get':86B 'ghsa':13A 'ghsa-qp9x-wp8f-qgjj':12A 'given':100C 'glob':109C,284C 'head':206C 'host':123C,131C,178C 'ident':127C,363C 'implement':202C 'includ':282C 'input':140C 'insensit':41B,164C 'jku':482C 'jussi':480C 'koda':475C 'kodareef5':477C 'kukkonen':481C 'legit':396C,455C 'len':265C,268C 'linux':43B 'linux/macos':160C,472C 'list':393C 'lowercas':138C 'maco':45B 'mainlin':330C 'make':168C,233C 'match':8A,35B,46B,120C,154C,226C,298C 'matter':444C 'must':435C,450C,465C 'name':334C,336C,338C,344C 'need':231C,422C 'ngclient':23B,34B,201C 'normal':359C 'normcas':124C,137C 'ntpath':135C 'one':437C 'oper':63B,179C 'order':84B,401C,443C,461C 'os.path':132C 'os.path.normcase':115C,337C,340C 'packag':24B 'part':257C,261C,267C,270C,277C,296C,306C,308C 'pat':335C,339C,341C,345C 'path':7A,38B,77B,102C,152C,374C,431C 'path-bas':373C 'pathpattern':216C,219C,228C,238C,280C,295C 'pathpattern.split':262C 'pattern':78B,110C,153C,260C,269C,285C,302C,307C,316C,378C,432C 'place':79B 'platform':4A,365C 'platform-depend':3A 'point':240C 'posix':122C 'pre':460C 'pre-ord':459C 'provid':415C 'python':25B,31B,111C,148C,199C,208C,325C,331C 'python-tuf':30B,147C,198C 'qgjj':16A 'qp9x':14A 'reef':476C 'replac':142C,347C 'report':474C 'repositori':53B,370C,424C 'resolv':133C 'result':146C,188C 'return':272C,318C,321C,342C 'role':71B,441C,449C,456C 'run':184C,466C 'search':405C 'sensit':49B,158C 'sourc':324C 'special':253C 'specif':194C 'staticmethod':210C 'str':218C,220C 'strict':309C 'sure':234C 'symbol':254C 'system':64B,180C 'target':37B,57B,88B,101C,174C,214C,256C,266C,293C,301C,305C,315C,416C 'targetpath':217C,225C,236C 'targetpath.split':258C 'threat':250C 'true':310C,322C 'tuf':1A,20A,26B,32B,149C,193C,200C,369C 'tuf/api/_payload.py':205C 'two':372C 'unchang':327C 'updat':186C 'use':94C 'violat':195C 'visit':452C 'vulner':203C 'walk':462C 'whether':98C,224C 'whose':136C,377C 'window':27B,29B,91B,130C,166C,190C,409C,468C 'wp8f':15A 'zip':304C","nicknames":[],"mitigations":[],"who_affected":"Teams that run python-tuf ngclient on Windows hosts to verify TUF repositories with path-based delegations. Exposure needs a repository whose delegation patterns differ only in case and an attacker-controlled delegated role visited first. Linux and macOS clients are not affected, and repositories without path-based delegations can skip it.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["tuf"],"ptokens":["tuf"],"labels":["cwe178","medium","ngclient","package","pypi","python","tuf","windows"],"campaign_type":null,"search_a":"'-123806':11A,19A '-2026':10A,18A 'cve':9A,17A 'deleg':6A 'depend':5A 'ghsa':13A 'ghsa-qp9x-wp8f-qgjj':12A 'match':8A 'path':7A 'platform':4A 'platform-depend':3A 'qgjj':16A 'qp9x':14A 'tuf':1A,20A 'wp8f':15A","search_b":"'-178':22B 'accept':89B 'attack':66B 'author':55B 'case':40B,48B,75B 'case-diff':74B 'case-insensit':39B 'case-sensit':47B 'client':61B,92B 'control':68B 'cwe':21B 'deleg':36B,70B,83B 'depend':58B 'differ':56B,76B 'earlier':80B 'get':86B 'insensit':41B 'linux':43B 'maco':45B 'match':35B,46B 'ngclient':23B,34B 'oper':63B 'order':84B 'packag':24B 'path':38B,77B 'pattern':78B 'place':79B 'python':25B,31B 'python-tuf':30B 'repositori':53B 'role':71B 'sensit':49B 'system':64B 'target':37B,57B,88B 'tuf':26B,32B 'window':27B,29B,91B","search_c":"'1':367C '1183':209C '1184':211C '1185':222C '1186':229C '1187':242C '1188':255C '1189':259C '1190':263C '1191':271C '1192':274C '1193':275C '1194':289C '1195':299C '1196':311C '1197':317C '1198':320C '2':388C '3':402C '3.12':326C '7ecb67d':207C 'across':364C 'advisori':478C 'appli':357C 'argument':118C 'attack':366C,390C,414C,421C,434C,447C 'attacker-control':446C 'author':104C,170C 'base':375C 'behav':362C 'bool':221C 'call':114C 'case':157C,163C,358C,382C,428C 'case-collid':427C 'case-insensit':162C 'case-sensit':156C 'caveat':419C 'client':183C,404C,464C 'code':204C 'collid':429C 'configur':425C 'control':436C,448C 'could':281C 'credit':473C 'current':329C 'decid':97C 'decis':171C 'def':212C,332C 'deleg':107C,151C,376C,391C,397C,400C,430C,440C,442C 'delegatedrole._is_target_in_pathpattern':93C 'depend':175C 'determin':223C 'differ':379C 'directori':245C 'document':354C 'doesn':248C 'edit':479C 'effect':470C 'everi':276C 'exampl':384C 'explicit':353C 'exploit':418C 'fals':273C,319C 'find':412C 'fix':346C 'fnmatch':247C,333C 'fnmatch.fnmatch':95C,113C,314C,323C,348C 'fnmatch.fnmatchcase':350C 'fnmatchcas':343C 'foo':385C,387C 'foo/something':407C,417C 'function':128C 'given':100C 'glob':109C,284C 'head':206C 'host':123C,131C,178C 'ident':127C,363C 'implement':202C 'includ':282C 'input':140C 'insensit':164C 'jku':482C 'jussi':480C 'koda':475C 'kodareef5':477C 'kukkonen':481C 'legit':396C,455C 'len':265C,268C 'linux/macos':160C,472C 'list':393C 'lowercas':138C 'mainlin':330C 'make':168C,233C 'match':120C,154C,226C,298C 'matter':444C 'must':435C,450C,465C 'name':334C,336C,338C,344C 'need':231C,422C 'ngclient':201C 'normal':359C 'normcas':124C,137C 'ntpath':135C 'one':437C 'oper':179C 'order':401C,443C,461C 'os.path':132C 'os.path.normcase':115C,337C,340C 'part':257C,261C,267C,270C,277C,296C,306C,308C 'pat':335C,339C,341C,345C 'path':102C,152C,374C,431C 'path-bas':373C 'pathpattern':216C,219C,228C,238C,280C,295C 'pathpattern.split':262C 'pattern':110C,153C,260C,269C,285C,302C,307C,316C,378C,432C 'platform':365C 'point':240C 'posix':122C 'pre':460C 'pre-ord':459C 'provid':415C 'python':111C,148C,199C,208C,325C,331C 'python-tuf':147C,198C 'reef':476C 'replac':142C,347C 'report':474C 'repositori':370C,424C 'resolv':133C 'result':146C,188C 'return':272C,318C,321C,342C 'role':441C,449C,456C 'run':184C,466C 'search':405C 'sensit':158C 'sourc':324C 'special':253C 'specif':194C 'staticmethod':210C 'str':218C,220C 'strict':309C 'sure':234C 'symbol':254C 'system':180C 'target':101C,174C,214C,256C,266C,293C,301C,305C,315C,416C 'targetpath':217C,225C,236C 'targetpath.split':258C 'threat':250C 'true':310C,322C 'tuf':149C,193C,200C,369C 'tuf/api/_payload.py':205C 'two':372C 'unchang':327C 'updat':186C 'use':94C 'violat':195C 'visit':452C 'vulner':203C 'walk':462C 'whether':98C,224C 'whose':136C,377C 'window':130C,166C,190C,409C,468C 'zip':304C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-05-28T22:46:13+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-qp9x-wp8f-qgjj","url":"https://github.com/advisories/GHSA-qp9x-wp8f-qgjj","severity":"medium","published_at":"2026-05-28T22:46:13+00:00","modified_at":"2026-11-09T20:18:56+00:00","first_seen_at":"2026-11-09T20:23:06+00:00","fetched_at":"2026-11-09T20:43:36+00:00","note":null}],"documents":[{"id":109671,"url":"https://api.github.com/repos/theupdateframework/python-tuf/security-advisories/GHSA-qp9x-wp8f-qgjj","title":"Maintainer's advisory: api.github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: api.github.com"},{"id":109672,"url":"https://github.com/theupdateframework/python-tuf/security/advisories/GHSA-qp9x-wp8f-qgjj","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"},{"id":109673,"url":"https://github.com/theupdateframework/python-tuf/releases/tag/v7.0.0","title":"Release: github.com","kind":"release","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Release: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}