Repository navigation
fix(files): .con was a truncated .conf, and the guard now checks every entry against a named list #16878
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(files): .con was a truncated .conf, and the guard now checks every entry against a named list #16878
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,3 +42,78 @@ def test_no_allowlist_holds_a_truncated_extension(): | |
| union = conversation_files.ALLOWED_EXTENSIONS | files.ALLOWED_EXTENSIONS | ||
| pairs = {(short, long) for short in union for long in union if short != long and long.startswith(short)} | ||
| assert pairs <= _LEGITIMATE_PREFIX_PAIRS, sorted(pairs - _LEGITIMATE_PREFIX_PAIRS) | ||
|
|
||
|
|
||
| # AC2 of #16521 asks for more than the prefix-pair check above: *every* entry in | ||
| # both allowlists must be a real extension spelling from a named list. | ||
| # | ||
| # The prefix check only catches a truncation whose full form is ALSO in the set — | ||
| # that is how `.pd`/`.pdf` and `.gi`/`.gif` presented. It cannot see a truncation | ||
| # standing alone, and one was: `.con` sat in files.py's config cluster | ||
| # (`.log`, `.cfg`, `.ini`, `.con`) with no `.conf` anywhere in the set, so no pair | ||
| # existed to flag. It had been there since #926, and the consequence was the same | ||
| # shape as the PDF bug — `.conf` uploads refused, `.con` accepted. | ||
| # | ||
| # `mimetypes.types_map` is the named list, plus an explicit set of real extensions | ||
| # Python's table simply lacks. That second set is the part that must stay honest: | ||
| # every addition needs to be a real spelling, not a convenient way to silence the | ||
| # guard, which is why each carries a reason. | ||
| _MIMETYPES_GAPS = { | ||
| ".cfg": "config file; not in Python's mimetypes table", | ||
| ".conf": "config file; not in Python's mimetypes table", | ||
| ".ini": "config file; not in Python's mimetypes table", | ||
| ".log": "plain-text log; not in Python's mimetypes table", | ||
| ".yaml": "YAML; absent from mimetypes before Python 3.13", | ||
| ".yml": "YAML; absent from mimetypes before Python 3.13", | ||
| # Executable formats from `files._DANGEROUS_EXTENSIONS`. Real spellings that | ||
| # Python has no media type for, which is unsurprising -- mimetypes maps what | ||
| # a browser should DISPLAY, and nothing should ever be served as these. | ||
| ".app": "macOS application bundle; executable, no media type", | ||
| ".cmd": "Windows batch script; executable, no media type", | ||
| ".pif": "Windows program-information file; executable, no media type", | ||
| ".vbs": "VBScript; executable, no media type", | ||
| } | ||
|
|
||
|
|
||
| def _extension_set_entries() -> set[str]: | ||
| """Every extension in every extension set in both modules, allow AND deny. | ||
|
|
||
| Deny sets are in scope deliberately, and the name says so because the first | ||
| version of this called them allowlists and was wrong about what it read. | ||
| ``files._DANGEROUS_EXTENSIONS`` matches ``name.isupper()`` -- a leading | ||
| underscore does not change that -- and including it is the right outcome, | ||
| not a leak: a truncated entry in a DENY list is worse than one in an allow | ||
| list. `.pd` in an allow list refuses a legitimate PDF; `.ex` in a deny list | ||
| would fail to block `.exe` and say nothing at all. | ||
| """ | ||
| found: set[str] = set() | ||
| for module in (conversation_files, files): | ||
| for name in dir(module): | ||
| if not name.isupper(): | ||
| continue | ||
| value = getattr(module, name) | ||
| if ( | ||
| isinstance(value, (set, frozenset)) | ||
| and value | ||
| and all(isinstance(v, str) and v.startswith(".") for v in value) | ||
| ): | ||
| found |= {v.lower() for v in value} | ||
| return found | ||
|
|
||
|
|
||
| def test_the_entry_scan_still_finds_the_extension_sets(): | ||
| """A zero here means the scan drifted, not that the sets are empty.""" | ||
| assert _extension_set_entries(), "no extension set found in either module — this guard is blind" | ||
|
|
||
|
|
||
| def test_every_extension_set_entry_is_a_real_extension(): | ||
| """A truncation standing alone has no prefix pair, so only a named list catches it (#16521).""" | ||
| import mimetypes | ||
|
|
||
| mimetypes.init() | ||
| known = {e.lower() for e in mimetypes.types_map} | set(_MIMETYPES_GAPS) | ||
|
Comment on lines
+113
to
+114
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,145p' autobot-backend/api/upload_allowlists_16521_test.py
python - <<'PY'
import inspect, mimetypes
print(inspect.getsource(mimetypes.init))
PY
rg -n '_MIMETYPES_GAPS|extension_set_entries|mimetypes\.init|ALLOWED_EXTENSIONS|DANGEROUS_EXTENSIONS' autobot-backend/apiRepository: mrveiss/AutoBot-AI Length of output: 9001 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Python version declarations ---'
rg -n --glob 'pyproject.toml' --glob 'setup.cfg' --glob 'setup.py' --glob 'Dockerfile*' --glob '*.yml' --glob '*.yaml' --glob '*.md' 'python_requires|requires-python|Python [0-9]|python-version|FROM python|3\.[0-9]+' . | head -200
printf '%s\n' '--- stdlib bindings and source ---'
python - <<'PY'
import inspect, mimetypes
print('python:', __import__('sys').version)
print('mimetypes module:', mimetypes.__file__)
print('MimeTypes.__init__:')
print(inspect.getsource(mimetypes.MimeTypes.__init__))
print('MimeTypes.read_windows_registry:')
print(inspect.getsource(mimetypes.MimeTypes.read_windows_registry))
PY
printf '%s\n' '--- test and nearby project configuration ---'
sed -n '1,135p' autobot-backend/api/upload_allowlists_16521_test.py
find autobot-backend -maxdepth 2 -type f \\( -name 'pyproject.toml' -o -name 'requirements*.txt' -o -name 'Dockerfile*' \\) -printRepository: mrveiss/AutoBot-AI Length of output: 33021 🌐 Web query:
💡 Result: <search_synthesis> <source_evidence> Citations:
🌐 Web query:
💡 Result: <search_synthesis> <source_evidence> Citations:
Use an isolated built-in MIME map for this guard.
Use a separate built-in map: known = {e.lower() for e in mimetypes.MimeTypes(filenames=[]).types_map[True]} | set(
_MIMETYPES_GAPS
)🤖 Prompt for AI Agents |
||
| unknown = sorted(_extension_set_entries() - known) | ||
| assert not unknown, ( | ||
| f"extension-set entries that are not real spellings: {unknown}. Either the entry is a " | ||
| f"truncation (the #16521 defect) or it is real and belongs in _MIMETYPES_GAPS with a reason." | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: mrveiss/AutoBot-AI
Length of output: 6871
Validate every declared extension set before collecting entries.
If
files.ALLOWED_EXTENSIONScontains"conf",all(...)is false and_extension_set_entries()skips the whole set. Another valid set still makes the non-empty assertion pass, so the malformed member is not checked. The helper docstring states that every allow and deny set is in scope; malformed sets are not intentionally excluded.Make the intended extension sets explicit, then assert that every member is a dotted string before adding it to
found.🤖 Prompt for AI Agents