{"schema":"threatline.record/2","record":{"id":"GHSA-r3c7-7xwm-6vfw","aliases":["GHSA-r3c7-7xwm-6vfw"],"summary":"Multi-byte identifier characters can silently change literal values","details":"## Summary\r\n\r\nA multi-byte character, such as a non-ASCII identifier or a byte-order mark, shifts Vyper's source lookup for later literals on the same line. Crafted source can make numeric literals compile with values or types that are not the ones written, without a compiler warning.\r\n\r\n## Details\r\n\r\nIn `vyper/ast/parse.py`, `line_index` counts characters, but Python's `col_offset` counts UTF-8 bytes within the line. Each multi-byte character shifts later source slices on that line right by its extra bytes. `visit_Num` classifies literals and reconstructs hexadecimal, binary and decimal values from these slices. A replacement placed where the slice lands therefore becomes the compiled value, and a plain integer can become a hex literal.\r\n\r\nTwo triggers are reachable:\r\n\r\n- Non-ASCII identifiers that NFKC-normalize to ASCII, such as `ﬁ`, `ｆｏｏ` or `𝚏𝚘𝚘`.\r\n- A byte-order mark at the start of the file, which shifts literals on line 1. It is invisible.\r\n\r\nThis shares its root cause with [GHSA-wvx7-39qj-2g9v](https://github.com/vyperlang/vyper/security/advisories/GHSA-wvx7-39qj-2g9v): the line table introduced in 0.4.1.\r\n\r\n## Proof of concept\r\n\r\nThe identifier uses mathematical monospace letters (4 bytes, 1 character each). They shift the lookup onto the address in the trailing comment:\r\n\r\n```python\r\nimport vyper\r\n\r\n# The identifier uses MATHEMATICAL MONOSPACE letters (4 UTF-8 bytes each),\r\n# which Python normalizes to the ASCII name TREASURY_ADDRESS_FOR_FEES.\r\nsource = \"\"\"𝚃𝚁𝙴𝙰𝚂𝚄𝚁𝚈_𝙰𝙳𝙳𝚁𝙴𝚂𝚂_𝙵𝙾𝚁_𝙵𝙴𝙴𝚂: constant(address) = 0x000000000000000000000000000000000000dEaD                      # 0x00000000000000000000000000000000c0Ffee11\r\n\r\n@external\r\n@view\r\ndef treasury() -> address:\r\n    return TREASURY_ADDRESS_FOR_FEES\r\n\"\"\"\r\n\r\n# Same contract with an ASCII identifier\r\nclean = source.replace(source.split(\":\")[0], \"TREASURY_ADDRESS_FOR_FEES\", 1)\r\n\r\n# The getter's runtime bytecode matches an explicitly substituted contract.\r\nsubstituted = clean.replace(\r\n    \"0x000000000000000000000000000000000000dEaD\",\r\n    \"0x00000000000000000000000000000000c0Ffee11\",\r\n    1,\r\n)\r\n\r\ndef runtime(code):\r\n    return vyper.compile_code(code, output_formats=[\"bytecode_runtime\"])[\"bytecode_runtime\"]\r\n\r\nassert runtime(source) == runtime(substituted)\r\nassert runtime(source) != runtime(clean)\r\n```\r\n\r\nBoth assertions pass.\r\n\r\n## Impact\r\n\r\nAn attacker able to contribute source code can change an address, selector, byte sequence, integer or decimal constant. Identifier triggers can pass as ordinary letters in review; a byte-order mark is invisible.\r\n\r\n## Affected versions\r\n\r\n| pip range                | Notes                                                                                      |\r\n| ------------------------ | ------------------------------------------------------------------------------------------ |\r\n| `>= 0.1.0b1, < 0.1.0b12` | Literals are read using Python's byte offsets.                                             |\r\n| `= 0.1.0b14`             | A parser refactor overwrites the `asttokens` character offsets with Python's byte offsets. |\r\n| `>= 0.4.1`               | Affected through 0.5.0b1; also unpatched on the inspected `master` checkout (`17e925db`).  |\r\n\r\n0.1.0b12, 0.1.0b13 and 0.1.0b15 through 0.4.0 use character offsets from `asttokens` and are unaffected.\r\n\r\n## Remediation\r\n\r\nConvert byte offsets to character offsets using each line's own text before slicing, and take literal values from the parser instead of source text. Until fixed, reject non-ASCII characters outside comments and docstrings, including a byte-order mark.\r\n","categories":["Unclassified"],"product":"vyper","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/vyperlang/vyper/security/advisories/GHSA-r3c7-7xwm-6vfw","source_modified_at":"2026-10-06T09:28:01Z","fetched_at":"2026-10-23T18:58:35.272684Z","package":{"ecosystem":"pip","name":"vyper"},"range":">= 0.1.0b1, < 0.1.0b12"},{"source":"GitHub Advisory Database","source_url":"https://github.com/vyperlang/vyper/security/advisories/GHSA-r3c7-7xwm-6vfw","source_modified_at":"2026-10-06T09:28:01Z","fetched_at":"2026-10-23T18:58:35.272684Z","package":{"ecosystem":"pip","name":"vyper"},"range":"= 0.1.0b14"},{"source":"GitHub Advisory Database","source_url":"https://github.com/vyperlang/vyper/security/advisories/GHSA-r3c7-7xwm-6vfw","source_modified_at":"2026-10-06T09:28:01Z","fetched_at":"2026-10-23T18:58:35.272684Z","package":{"ecosystem":"pip","name":"vyper"},"range":">= 0.4.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/vyperlang/vyper/security/advisories/GHSA-r3c7-7xwm-6vfw","source_modified_at":"2026-10-06T09:28:01Z","fetched_at":"2026-10-23T18:58:35.272684Z","package":{"ecosystem":"pip","name":"vyper"},"version":"0.1.0b12"},{"source":"GitHub Advisory Database","source_url":"https://github.com/vyperlang/vyper/security/advisories/GHSA-r3c7-7xwm-6vfw","source_modified_at":"2026-10-06T09:28:01Z","fetched_at":"2026-10-23T18:58:35.272684Z","package":{"ecosystem":"pip","name":"vyper"},"version":"0.1.0b15"}],"cvss":null,"severity":"Low","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-06T09:28:01Z","modified_at":"2026-10-06T09:28:01Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/vyperlang/vyper/security/advisories/GHSA-r3c7-7xwm-6vfw","fetched_at":"2026-10-23T18:58:35.272684Z","source_modified_at":"2026-10-06T09:28:01Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-06T09:28:01Z","content_hash":"71392e85599d9ee4387a475f7863c23a35f752b10e5fb6dfa4aa5a6b6896dfa4"}}
