fix(security): strip directory components from Teams recording display_name to prevent path traversal - #28173
Conversation
…y_name to prevent path traversal
|
Did a local stack audit. The Teams path-traversal hardening is valid, but this PR is stacked on unrelated mobile dashboard/web commits (head includes 6fa1701 / #28127). I opened a clean replacement with the security fix plus regression coverage here: #28177. Verification on the clean branch: scripts/run_tests.sh tests/plugins/test_teams_pipeline_plugin.py -> 11 passed. |
|
BoardJames CI triage:
No branch fix pushed; recommendation is to re-run/merge once the shared full-suite CI budget issue is fixed or the test workflow timeout is raised/split. |
|
Thanks @outsourc-e for the test coverage and salvage! Authorship |
|
Recommendation: needs rework; keep this PR superseded rather than merging it as-is. Checked current Validation:
Since #28177 is already the clean replacement with the focused regression test, I would not merge #28173. Please carry the fix forward in the replacement by rejecting empty, Signed: GPT-5.5-xhigh in Codex |
|
Thanks @egilewski for the thorough audit — the |
|
For the record: your Teams recording path-traversal fix (this PR's commit, authored by @memosr) shipped to main via #56198, with your authorship preserved in git log. The clean superset was routed through #28177; the merged version also closes a follow-up edge case (bare '..'/'.'/'' display names). Thanks! |
What does this PR do?
The Teams meeting pipeline (
plugins/teams_pipeline/pipeline.py,landed in v0.14.0 via #22007) downloads meeting recording artifacts
to a per-job temp directory before passing them to STT. The local
filename used to write the artifact comes straight from the Graph
API's
recording.display_name:Path / user_inputdoes not strip..segments — it justconcatenates. If
display_namecontains directory components, theresulting
recording_pathescapestmp_dir.Attack scenario
recording.display_nameis sourced frompayload.get("displayName")inmeetings.py:102, which is therecording's
displayNamefield as returned by the Graph/communications/callRecords/{id}/sessions/{sid}/recordingsendpoint. That field is ultimately set by the meeting organizer
when the recording is created.
A meeting organizer (any M365 user with rights to record a meeting
the agent is connected to) creates a recording with:
When the pipeline picks the artifact up:
download_recording_artifactthen opens that path for writing andstreams the attacker-controlled recording bytes into it. The
attacker now controls a file at an arbitrary path the Hermes
process can write to — common targets include:
~/.ssh/authorized_keys(any user-writable path)~/.hermes/auth.json(overwrite agent credentials)/etc/cron.d/...(if Hermes runs as root, e.g. some Docker setups)~/.bashrc,~/.zshrc(next-shell-spawn code execution)The artifact body is fully attacker-controlled — they uploaded
whatever they wanted as the recording content. Combined with the
filename traversal, that's an arbitrary write primitive triggered
by every meeting the agent processes.
CVSS 3.1 estimate
AV:N/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N→ 7.3 (HIGH)PR:L because the attacker needs to be a meeting participant /
organizer the agent is connected to, not anonymous. S:C and I:H
because arbitrary file write at the Hermes process's privilege
level can pivot to credential theft, persistence, or code
execution depending on what's writable.
Fix
One-line — strip directory components with
Path(...).name:Path("../../etc/cron.d/evil").name→"evil"— directorycomponents are dropped, only the basename survives. The
or f"{...}.mp4"fallback covers the edge case where thesanitized name is empty (e.g.
display_name = "../"→"").Why this matters
meetings.py:201already handlesdisplay_namecorrectly — ituses
Path(display_name).suffixwhich is safe.pipeline.py:461was joining the raw value into a path.change for legitimate recordings whose displayName is just a
human-readable label.
Mirrors the defense-in-depth pattern in:
#21277— dashboard plugin SRI integrity#19597— Meet node localhost binding + chmod#22432— Google Chat sender_type coercion#27825— LSP diagnostic sanitization (in review)Type of Change
Checklist