fix(skills): redact secrets on every mural CLI output channel - #2775
Open
Jamie Kim (jkim323) wants to merge 9 commits into
Open
fix(skills): redact secrets on every mural CLI output channel#2775Jamie Kim (jkim323) wants to merge 9 commits into
Jamie Kim (jkim323) wants to merge 9 commits into
Conversation
- redact MuralError text in all main() stderr handlers - add _emit_json_error and route the four error envelopes - make _redact_payload key-aware and cover tuples and sets - bound transport error excerpts via _error_excerpt - update SECURITY.md B4 and add gap G-INF-5 🔒 - Generated by Copilot
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2775 +/- ##
==========================================
- Coverage 82.98% 78.87% -4.12%
==========================================
Files 183 113 -70
Lines 33787 15779 -18008
Branches 25 25
==========================================
- Hits 28038 12445 -15593
+ Misses 5746 3331 -2415
Partials 3 3
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Contributor
Eval Execution✅ Status: Passed
No changed AI artifacts required evaluation. |
…f sensitive information' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
- pin MURAL_CREDENTIAL_BACKEND=file so resolve_backend cannot reach auto - reuse _isolate_credential_env to strip credential env vars - compare key sets so a failure reports names, not secret values 🔒 - Generated by Copilot
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request
Description
The mural CLI leaked unredacted secret material on multiple output channels.
main()printed rawMuralErrortext to stderr, the four structured error envelopes were serialized without redaction,_emit_jsonwrote unredacted payloads to stdout, and_transport.pyembedded raw response bodies into error messages. OAuth responses, refresh-token failures, and asset-upload failures could therefore surface bearer tokens, client secrets, and SAS query strings in terminal output and CI logs.This PR turns redaction into a structural barrier at the emit boundary rather than a text filter applied after serialization, then extends that barrier to every channel a later security sweep found still uncovered.
Core redaction barrier
main()stderr handlers pass error text through_redactbefore printing._emit_json_errorredacts and emits error envelopes to stderr; the four envelopes (human_authored_widget_protected,tag_merge_conflict,AREA_CAPACITY_EXCEEDED,bulk_atomic_abort) route through it._redact_payloadredacts the payload structure before serialization. Regex-scrubbing serialized JSON was structurally unsafe: the form-value pattern consumed closing quotes and produced output that no longer parsed, and escaped quotes in nested strings defeated the JSON pattern. It walksstr,dict,list,tuple,set, andfrozenset, and is key-aware, so a mapping key matching_REDACT_KEYSis masked regardless of its value type or quoting. That is strictly stronger than the previous string-level behavior._emit_json(stdout) redacts through the same barrier._error_excerptin_transport.pyredacts and then truncates response bodies atMAX_ERROR_EXCERPT_CHARS(512). Ordering is deliberate: redacting first means truncation cannot sever a key name from its value and defeat a substitution.Follow-on hardening
A security sweep of this branch (OWASP Top 10, MCSB, Secure by Design, OWASP MCP, OWASP Agentic) found five coverage gaps in the barrier above. All five are closed here.
re.IGNORECASEwhile_validate_asset_urlnormalizes withparsed.hostname.lower(), so a mixed-case host such asacct.Blob.Core.Windows.Netpassed validation and then defeated redaction, putting a fullsig=into a debug log line. A standalonesig=pattern was added as well, because an upstream error body can quote SAS parameters without the URL and truncation can strip the host prefix._build_api_errornow shares the excerpt bound._error_excerptcovered five of six exception-construction sites. The sixth carries every non-2xx API response and was bounded only byMURAL_MAX_BODY_BYTES(16 MiB default), four orders of magnitude above the 512-character cap._emit_recordsand_emit_recordprinted_format_output(...)directly while_emit_jsonredacted, which left the primary stdout data path as the only channel without a barrier.removed.append(key)withremoved.append("***")in_logout_remove_credentials. The masked values are the env-style identifiers in_KNOWN_CREDENTIAL_KEYS, not credential values, somural auth logoutreported(keys: ***, ***, ***)and no secret was ever protected. The change also left the equivalenterrors[key]path uncut, so it did not achieve its own stated goal. Reverted, and code scanning alert 723 is dismissed as a false positive: the expression it flags is the redaction sink itself, which CodeQL does not model as a sanitizer.SECURITY.mdanddocs/security/security-model.mddescribed an "embedded stdio MCP server" that does not exist. The package registers nomcpsubcommand, implements no JSON-RPC framing, and exposes no tool dispatch; the only MCP artifact is aMCPInvalidParamsErrorvalidation exception documented as retained for CLI helper compatibility. The repo-wide model additionally carried a full "Mural Skill MCP Server" entry asserting apython -m mural mcpdeployment and scope re-checks "at dispatch". Both now describe the real surface: an argparse CLI whose stdout and stderr are captured into agent context through a terminal tool. The genuine hardening items from that entry were relocated rather than dropped.Test isolation fix
test_autoload_credentials_returns_none_when_file_absentnever pinned a credential backend, soresolve_backendfell through toautoand read the developer's real keyring. It failed on any machine holding real Mural credentials, and its assertion form (assert "MURAL_CLIENT_ID" not in env) made pytest render the whole mapping into the failure message, printing live secret values. It now pinsMURAL_CREDENTIAL_BACKEND=file, reuses the existing_isolate_credential_envhelper, and compares key sets so a failure reports names only.Out of scope: the residual
print(*)sites in_cli_auth.py,_commands.py, and_operations.py, which #2756 explicitly defers to #2460. The security sweep also recorded lower-severity residuals (mapping-key redaction, argparse writing raw argv before the guarded block, SAS scope and expiry validation) that are not addressed here.Related Issue(s)
Fixes #2756
Type of Change
Code & Documentation:
AI Artifacts:
.github/skills/*/SKILL.md)Other:
.ps1,.sh,.py)Testing
Run from
.github/skills/experimental/mural:uv run ruff check .- all checks passeduv run ruff format --check .- 44 files already formatteduv run pytest tests -q- 860 passed, 143 skipped, 0 failedThe suite has no failures. The one previously failing test is the environment-dependent credential test described above, fixed in this PR.
Repository gates:
npm run lint:py,npm run lint:frontmatter,npm run lint:tables, andnpm run validate:skillsall clean (74 skills, 0 errors, 0 warnings).44 test functions were added across four files:
tests/test_redaction.py(29) -main()error sinks and source-level pins,_redact_payloadcontainer coverage, sensitive mapping keys,_emit_jsonstdout channel, SAS host casing and bare-signature masking, record-channel redactiontests/test_mural_transport.py(11) -_error_excerptunit behavior,_build_api_errorbounding and masking, end-to-end transport coveragetests/test_logout_transparency.py(4) -removed_keyscontent and rendered summary, which no test previously pinnedtests/test_credential_storage.py- the isolation fix described aboveEach security fix was checked for vacuity by temporarily reverting the source change and confirming the new tests fail. That check earned its keep: it caught that two of the four logout-transparency tests exercise only the renderer and pass against the regressed code, so the coverage claim is scoped to the two that do fail.
Checklist
Required Checks
Security Considerations
Additional Notes
Two points worth a reviewer's attention.
Alert 723 is dismissed, not fixed. CodeQL flags
print(json.dumps(_redact_payload(payload), indent=2))in_emit_json, which is the redaction sink introduced by this PR._redact_payloadis not modeled as a sanitizer, so the safest path in the module is the one most likely to be flagged. Reverting the Autofix restores the flagged dataflow, which is why the dismissal is a prerequisite rather than a cleanup step. Modeling the sanitizer properly is tracked as follow-up work.Record-channel redaction can over-mask user content. Routing
_emit_recordsthrough_redact_payloadmeans a Mural sticky note whose text legitimately readscode=ABC123now renders ascode=***in list output. That is a real behavior change on the primary data path, accepted because the alternative leaves the busiest stdout channel as the only one without a barrier, and because_emit_jsonalready accepts the same trade-off for envelopes.