Developer Docs Security Reference
PHASE 15

Security Reference

Fourteen layered security guards in muse/core/validation.py and adjacent modules. Every guard is documented with the attack it prevents, the function that implements it, and every site where it is applied.

Threat model

Muse operates in three environments — local developer machines, CI pipelines, and multi-agent clusters running hundreds of commits per minute. The threat surface shifts between them, but the guards apply everywhere.

Threat Severity Guard
Path traversal via manifest keys CRITICAL contain_path() — zip-slip defence
Path traversal via object IDs CRITICAL validate_object_id() — hex-only, no slashes or dots
XXE / Billion Laughs via XML CRITICAL SafeET — defusedxml wrapper
Authorization header leakage via redirect CRITICAL _NoRedirectHandler in HttpTransport
ANSI/OSC escape injection via commit messages HIGH sanitize_display() — strips C0/C1 controls
Null-byte / CR injection via branch names HIGH validate_branch_name() — allowlist + reject rules
Symlink escape during checkout HIGH contain_path() resolves symlinks before containment check
Identity store TOCTOU race HIGH os.open() + fchmod() + atomic rename + flock
Glob injection in prefix lookup MEDIUM sanitize_glob_prefix() — strips metacharacters
OOM via unbounded HTTP response MEDIUM MAX_RESPONSE_BYTES = 64 MB cap in HttpTransport
NaN propagation in numeric computation MEDIUM finite_float() — returns fallback for Inf/NaN
Scheme downgrade (HTTPS → HTTP) MEDIUM HTTPS enforcement in _build_request()
Hash collision via separator ambiguity MEDIUM Null-byte separator in compute_snapshot_id()
Credential capture via dotfile snapshot MEDIUM walk_workdir() excludes symlinks and hidden files

Trust boundary

All data from outside the trust boundary is treated as adversarial. Validation happens at the boundary — never inside the trusted zone.

Trust boundary diagram
Untrusted input
remote push payloads
CLI arguments
branch names
commit messages
manifest keys
HTTP responses
XML / SysEx data
file:// URLs
Validation layer
validate_object_id()
validate_branch_name()
contain_path()
sanitize_display()
sanitize_glob_prefix()
clamp_int()
finite_float()
SafeET.parse()
Trusted zone
object store
commit DAG
merge engine
Harmony
identity store
snapshot manifest

All fourteen guards at a glance

Object ID Validation path traversal
validate_object_id(object_id)

Rejects any object ID that is not exactly 64 lowercase hex characters. Blocks path traversal via crafted IDs like ../../.ssh/authorized_keys. Content integrity check: write_object() verifies the written content actually hashes to the provided ID.

Applied at: object_path(), restore_object(), write_object(), resolve_commit_ref(), store_pulled_commit(), read_merge_state(), apply_resolution()
Branch Name Validation path / log injection
validate_branch_name(name)

Rejects backslashes, null bytes, CR/LF, leading/trailing dots, consecutive dots, consecutive slashes, and names longer than 255 characters. Prevents branch names from escaping ref path construction or injecting into log output.

Applied at: init, commit, branch, checkout -b, merge, reset, get_head_commit_id(), LocalFileTransport.push_pack()
Path Containment zip-slip / symlink escape
contain_path(base, rel) → Path

Joins base / rel, resolves all symlinks, then asserts the result is inside base. A manifest key like ../../.ssh/authorized_keys or a pre-placed symlink pointing outside the workdir are both caught.

Applied at: checkout, merge, reset --hard, revert, cherry_pick, shelf pop, all 7 semantic write commands, read_merge_state(), apply_resolution(), LocalFileTransport
ANSI Injection Defence terminal escape
sanitize_display(s)

Strips C0 control characters (\x00–\x1f except tab and newline), DEL (\x7f), and C1 controls (\x80–\x9f). Prevents commit messages or branch names from injecting OSC/CSI terminal escape sequences when echoed.

Applied at all typer.echo() paths that output user-controlled strings: log, tag, branch, checkout, merge, reset, revert, cherry_pick, commit
Glob Injection Prevention filesystem scan
sanitize_glob_prefix(prefix)

Strips glob metacharacters * ? [ ] { } before Path.glob() is called. Without this, a crafted prefix like **/* would enumerate the entire directory tree rooted at .muse/commits/.

Applied at: _find_commit_by_prefix() in store.py
Numeric Guards resource exhaustion / NaN
clamp_int(val, lo, hi, name) · finite_float(val, fallback)

clamp_int raises ValueError for out-of-range integers. finite_float returns a safe fallback for Inf, -Inf, NaN. Prevents resource exhaustion via large arguments and silent computation corruption via NaN propagation.

Applied at: log --max-count, find_phrase --depth/--min-score, humanize --timing/--velocity, invert --pivot, MIDI parser tempo and divisions
XML Safety XXE / Billion Laughs / SSRF
SafeET — muse/core/xml_safe.py

Wraps defusedxml.ElementTree.parse() behind a typed class. Prevents Billion Laughs (exponential entity expansion), XXE (external entity credential theft via file:///etc/passwd), and SSRF via XML.

Applied at: all XML file parsing in the MIDI domain and any MusicXML import paths. Use SafeET.parse(), never xml.etree.ElementTree.parse().
HTTP Transport Hardening auth leak / downgrade / OOM
HttpTransport — muse/core/transport.py

Four controls: (1) redirect refusal via _NoRedirectHandler — prevents Authorization: MSign header leakage to redirected hosts; (2) HTTPS enforcement — rejects non-HTTPS URLs; (3) MAX_RESPONSE_BYTES = 64 MB cap — prevents OOM; (4) content-type guard — checks first byte is { or [ before JSON parsing.

Applied at: all muse push, muse fetch, muse hub, muse auth operations that make outbound HTTP requests to MuseHub or staging
Local File Transport Hardening symlink traversal
LocalFileTransport — muse/core/transport.py

Three controls for file:// URLs: (1) symlink canonicalisation via resolve() on the repo root path; (2) validate_branch_name() before any I/O; (3) contain_path() on the final ref path — defence-in-depth against pre-placed symlinks in .muse/refs/heads/.

Applied at: _repo_root(), push_pack()
Snapshot Integrity hash collision / credential leak
compute_snapshot_id() · walk_workdir()

Null-byte separator between key and value in compute_snapshot_id() makes filename/ID separator collisions structurally impossible (null bytes cannot appear in POSIX paths). walk_workdir() skips symlinks and dotfiles — prevents .env, .muse/, and other credential files from being captured in snapshots.

Applied at: every muse commit, muse shelf save, and snapshot creation path
Identity Store Security credential protection
muse/core/identity.py

Seven layered controls: 0o700 directory permissions; 0o600 file permissions set before first byte is written (no TOCTOU window); atomic rename via temp file + os.replace(); symlink guard before write; exclusive write lock via fcntl.flock; token masking in logs ("MSign ***"); URL normalisation so https://admin:secret@host/ and host resolve to the same key.

Applied at: muse auth keygen, muse auth register, muse auth rotate, all identity read/write operations
Size Caps OOM / DoS
MAX_FILE_BYTES · MAX_RESPONSE_BYTES · MAX_SYSEX_BYTES

Hard limits on per-blob reads (256 MB), HTTP response bodies (64 MB), and SysEx data per MIDI message (64 KiB). A crafted request or response that exceeds these limits is rejected before the bytes are buffered in memory.

object_store.read_object() · transport._execute() · midi_merge._msg_to_dict() · midi_parser.parse_file()

Size caps quick reference

Constant Value Where enforced
MAX_FILE_BYTES 256 MB object_store.read_object() — per-blob reads
MAX_RESPONSE_BYTES 64 MB transport._execute() — HTTP response body
MAX_SYSEX_BYTES 64 KiB midi_merge._msg_to_dict() — SysEx data per message
MIDI file size MAX_FILE_BYTES midi_parser.parse_file() — cap before parse begins