Skip to content

fix(media): media-delivery denylist is inert on Windows - #78162

Open
bbasketballer75 wants to merge 2 commits into
NousResearch:mainfrom
bbasketballer75:fix/media-denylist-windows-system-paths
Open

fix(media): media-delivery denylist is inert on Windows#78162
bbasketballer75 wants to merge 2 commits into
NousResearch:mainfrom
bbasketballer75:fix/media-denylist-windows-system-paths

Conversation

@bbasketballer75

Copy link
Copy Markdown

The bug

_MEDIA_DELIVERY_DENIED_PREFIXES is POSIX-only, and the entries don't degrade gracefully. Path("/etc").resolve() becomes C:\etc on the current drive — which doesn't exist. So on Windows every entry in that tuple is inert and the default-mode denylist is effectively empty.

Measured against unmodified main on Windows:

/etc     -> C:\etc      exists=False
/sys     -> C:\sys      exists=False
/root    -> C:\root     exists=False
...all nine resolve to nonexistent paths

C:\Windows      covered=False
C:\ProgramData  covered=False

Verified end-to-end through the guard itself, not just the constant:

validate_media_delivery_path(r"C:\Windows\win.ini")
  before -> <path>   # deliverable as a gateway attachment
  after  -> None     # refused

Strict mode was unaffected — the ~/.ssh denial works, because those entries are built from the live $HOME rather than hard-coded POSIX roots. This is the default (non-strict) path, which is what most operators run.

The fix

Two Windows-specific lists:

System roots, resolved from %SystemRoot% and %ProgramData% — read from the environment rather than hard-coded, because Windows needn't live on C: and a roaming profile needn't sit under C:\Users.

Credential stores under the user profile that have no POSIX counterpart: Microsoft\Credentials (roaming and local), Microsoft\Protect (DPAPI master keys), Microsoft\Crypto, Local\Microsoft\Vault. The POSIX dotfile equivalents (.aws, .ssh, .azure, .gcloud) use the same names on Windows and are already covered by _MEDIA_DELIVERY_DENIED_HOME_SUBPATHS.

AppData is deliberately not denied wholesale. %LOCALAPPDATA%\Temp is a normal home for generated artifacts, so a blanket rule would break legitimate media delivery. A test asserts Temp, Pictures and Downloads stay deliverable.

Verification (Windows)

  • 9 new tests. 6 fail when the production change is reverted — so the coverage pins real behaviour rather than passing vacuously. The 3 that pass either way are the must-stay-deliverable assertions.
  • tests/gateway/test_platform_base.py: 2 failed before, 2 failed after. Set-diff of failure names is empty — nothing newly broken. Those 2 are unrelated pre-existing failures.
  • ruff check clean.

Relationship to #52045

Complementary, not overlapping. #52045 rewrites the comparison helpers to be case-insensitive (_path_compare_key, _path_under_or_equal); this fixes what is in the list to compare against. Different hunks in the same file — #52045 touches the helpers around :52 and _path_under_denied_prefix at :1134, this touches the constants at :1196 and _media_delivery_denied_paths at :1333.

Both are needed: case-folding a denylist that contains only nonexistent paths still denies nothing.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings August 4, 2026 03:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Windows-specific security gap in gateway media attachment delivery: the default (non-strict) media-delivery denylist previously consisted of POSIX roots that resolve to nonexistent paths on Windows, making the denylist effectively empty there. This change adds Windows-appropriate denied roots and validates the behavior with Windows-only regression tests.

Changes:

  • Add Windows-specific denied roots sourced from %SystemRoot% and %ProgramData%, plus additional user-profile credential store subpaths.
  • Extend _media_delivery_denied_paths() to include the Windows deny entries when running on os.name == "nt".
  • Add Windows-only tests asserting system roots and credential stores are denied while common user directories remain deliverable, plus an end-to-end refusal check for a system file.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
gateway/platforms/base.py Adds Windows denylist entries and wires them into _media_delivery_denied_paths() for non-strict media delivery safety.
tests/gateway/test_platform_base.py Adds Windows-only regression tests covering denylist coverage and end-to-end refusal behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/gateway Gateway runner, session dispatch, delivery platform/windows Native Windows-specific behavior or breakage area/auth Authentication, OAuth, credential pools P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Aug 4, 2026
@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The new credential-store entries are still constructed under expanduser("~"), so they do not follow Windows' actual Roaming AppData known folder when it is redirected outside %USERPROFILE%. With %USERPROFILE%=C:\Users\svc and %APPDATA%=Z:\RoamingProfile\svc, the current-main replay correctly refuses C:\Users\svc\AppData\Roaming\Microsoft\Credentials\... but still returns Z:\RoamingProfile\svc\Microsoft\Credentials\... as deliverable in the default media path. Please derive the roaming/local credential roots from the Windows known-folder locations (or the corresponding %APPDATA% / %LOCALAPPDATA% values with a fail-closed fallback) and exercise a redirected-root case through validate_media_delivery_path().

Security evidence:

  • trust boundary: a model-emitted local path crosses into a native gateway attachment sourced from a Windows credential store.
  • source/sink/invariant: _media_delivery_denied_paths() must cover the actual credential known-folder roots before default-mode validate_media_delivery_path() returns an existing file.
  • current-main reproduction: both the default home-relative Credential Manager path and the redirected Roaming AppData path were outside the denylist.
  • PR-head or patch-replay validation: the conflict-free current-main replay denies the default-layout credential path, but its production validator still returns the redirected credential path.
  • positive/negative cases: C:\Users\svc\AppData\Roaming\Microsoft\Credentials\... changes from allowed to denied, while Z:\RoamingProfile\svc\Microsoft\Credentials\... remains allowed.
  • residual bypass search: the system-root entries, home exception, safe-root precedence, Windows containment semantics, %SystemRoot%, %ProgramData%, %APPDATA%, and %LOCALAPPDATA% were checked; the redirected per-user roots are the uncovered sibling.
  • reviewer validation: the invocation probe printed the exact imported module and changed the default-layout result to None while leaving the redirected result deliverable; the focused platform suite passed 85 tests with 10 skips.

Not checked:

  • Native Windows execution
  • Full test suite
  • CodeRabbit review

Signed: GPT-5.6-sol-xhigh in Codex

bbasketballer75 and others added 2 commits August 13, 2026 13:39
_MEDIA_DELIVERY_DENIED_PREFIXES is POSIX-only, and the entries do not degrade
gracefully. Path("/etc").resolve() becomes C:\etc on the current drive, which
does not exist -- so on Windows every entry in that tuple is inert and the
default-mode denylist is effectively empty.

Measured on Windows against unmodified main:

    /etc     -> C:\etc      exists=False
    /sys     -> C:\sys      exists=False
    /root    -> C:\root     exists=False
    ... all nine resolve to nonexistent paths

    C:\Windows      covered=False
    C:\ProgramData  covered=False

Consequence, verified end-to-end through the guard itself:

    validate_media_delivery_path(r"C:\Windows\win.ini")
      before -> <path>   (deliverable as a gateway attachment)
      after  -> None     (refused)

Strict mode was unaffected -- the ~/.ssh denial works, because those entries
are built from the live $HOME rather than hard-coded POSIX roots. This is the
default (non-strict) path, which is what most operators run.

Adds two Windows-specific lists:

  - system roots resolved from %SystemRoot% and %ProgramData%, read from the
    environment rather than hard-coded, because Windows need not live on C:
    and a roaming profile need not sit under C:\Users.

  - the credential stores under the user profile that have no POSIX
    counterpart: Microsoft\Credentials (both roaming and local),
    Microsoft\Protect (DPAPI master keys), Microsoft\Crypto, and
    Local\Microsoft\Vault. The POSIX dotfile equivalents (.aws, .ssh, .azure,
    .gcloud) use the same names on Windows and are already covered by
    _MEDIA_DELIVERY_DENIED_HOME_SUBPATHS.

AppData is deliberately NOT denied wholesale. %LOCALAPPDATA%\Temp is a normal
home for generated artifacts, so a blanket rule would break legitimate media
delivery. A test asserts Temp, Pictures and Downloads stay deliverable.

Verification on Windows:
  - 9 new tests; 6 of them fail when the production change is reverted, so the
    coverage pins real behaviour rather than passing vacuously. The 3 that pass
    either way are the must-stay-deliverable assertions.
  - tests/gateway/test_platform_base.py: 2 failed before, 2 failed after --
    set-diff of failure names is empty, nothing newly broken. Those 2 are
    unrelated pre-existing failures.
  - ruff clean.

Complementary to (not overlapping) the case-folding work in NousResearch#52045: that PR
rewrites the comparison helpers, this one fixes what is in the list to compare
against. Different hunks in the same file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lders

Addresses @egilewski's review: the credential-store entries were built
solely from ``expanduser("~")``, so they only matched the default profile
layout. Windows Known Folder redirection (roaming profiles, Folder
Redirection GPO) can point %APPDATA% / %LOCALAPPDATA% outside
%USERPROFILE% entirely — e.g. %USERPROFILE%=C:\Users\svc while
%APPDATA%=Z:\RoamingProfile\svc. In that layout the home-relative path was
denied but the *live* credential store at Z:\... stayed deliverable, which
is the path that actually holds the secrets.

Split the subpaths by which AppData root they live under and resolve each
against BOTH forms:

  - the env-derived root (%APPDATA% / %LOCALAPPDATA%), which is correct
    under redirection, and
  - the home-relative root, which is correct on the common layout and acts
    as a fail-closed fallback when the env vars are unset.

Denying both costs a few extra entries and no I/O, and avoids trading one
uncovered layout for another.

Verified with the reviewer's exact scenario (%APPDATA%=Z:\RoamingProfile\svc,
%LOCALAPPDATA%=Z:\LocalProfile\svc): all 5 redirected credential roots are
now denied while the 33 home-relative entries are retained.

tests/gateway/test_platform_base.py: 86 passed, 8 failed, 1 skipped —
byte-identical to the pre-change baseline on this Windows box (the 8 are
pre-existing POSIX-path/Docker-translation failures, unrelated to this
change; same test names before and after).
@bbasketballer75
bbasketballer75 force-pushed the fix/media-denylist-windows-system-paths branch from f8987f2 to 71c6a8c Compare August 13, 2026 17:54
@bbasketballer75

Copy link
Copy Markdown
Author

Fixed in 71c6a8c3 — you were right, and the redirected case was the live one.

The credential subpaths are now split by which AppData root they belong to and resolved against both the env-derived root (%APPDATA% / %LOCALAPPDATA%, correct under Known Folder redirection) and the home-relative root (correct on the default layout, and a fail-closed fallback when the env vars are unset). Denying both costs no extra I/O and avoids trading one uncovered layout for another.

Verified with your exact scenario (%APPDATA%=Z:\RoamingProfile\svc, %LOCALAPPDATA%=Z:\LocalProfile\svc) — all 5 redirected credential roots are now in _media_delivery_denied_paths(), while the 33 home-relative entries are retained:

redirected roots denied : 5
   Z:\RoamingProfile\svc\Microsoft\Credentials
   Z:\RoamingProfile\svc\Microsoft\Protect
   Z:\RoamingProfile\svc\Microsoft\Crypto
   Z:\LocalProfile\svc\Microsoft\Credentials
   Z:\LocalProfile\svc\Microsoft\Vault

Also rebased onto current main (was 1459 behind; clean, no conflicts).

tests/gateway/test_platform_base.py: 86 passed / 8 failed / 1 skipped — byte-identical to the pre-change baseline on this Windows box (same 8 test names before and after), so no regression from this commit. Those 8 are pre-existing POSIX-path and Docker-mount-translation failures unrelated to this change.

🤖 Addressed by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/windows Native Windows-specific behavior or breakage sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants