Skip to content

fix(security): close three untrusted-content gaps in AI and MCP paths - #405

Merged
jongio merged 1 commit into
mainfrom
fix/ai-mcp-hardening
Aug 5, 2026
Merged

jongio merged 1 commit into
mainfrom
fix/ai-mcp-hardening

Conversation

@jongio

@jongio jongio commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Closes the three untrusted-content findings from the release MQ pass. None were release blockers, all are cheap to fix.

Conflict regions bypassed prompt sanitization (CWE-1427)

buildConflictUserPrompt wrapped full file content in boundary markers but wrote the individual conflict regions raw. Since region text comes from the branches being merged, a crafted branch could inject instructions into the resolution prompt. Regions now go through SanitizeExternalContent, which also defuses any embedded terminator. Per-region human approval was already required, which is why this stayed medium rather than high.

Tool results were never redacted (CWE-201)

sendMessage redacts user input fail-closed, but handleToolResults only boundary-marked. Tool results carry file contents and git output, and the redactor's filename denylist only covers names, so a docker-compose.yml or terraform.tfvars with an inline credential reached the provider intact. Tool results now redact the same way, falling back to the placeholder on error.

file_list leaked sensitive filenames (CWE-200)

file_read and file_write both call IsSensitivePath; file_list did not. Reading was blocked but listing still exposed the names and sizes of .env files, SSH keys, and credential stores. Benign dot-files like .gitignore still list, since the denylist already allowlists them.

Testing

go test ./internal/mcp/... ./internal/chat/... ./internal/ai/... passes. Each fix has a regression test, including one asserting an injected [EXTERNAL_DATA_END] in conflict text gets defused rather than passed through.

Conflict region text bypassed the boundary-marker wrapping applied to
the surrounding file content, so a crafted branch could inject
instructions into the resolution prompt (CWE-1427). Regions now go
through SanitizeExternalContent like everything else, which also
defuses any embedded terminator.

Chat tool results were boundary-marked but never secret-redacted before
reaching the provider. The filename denylist only covers names, so a
docker-compose.yml or terraform.tfvars holding an inline credential
sailed through. Tool results now redact fail-closed, matching how user
input is handled in sendMessage (CWE-201).

file_list did not apply IsSensitivePath, so it leaked the names and
sizes of .env files, SSH keys, and credential stores even though
file_read blocks reading them (CWE-200). Benign dot-files such as
.gitignore still list.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c5a9c45a-0341-46e5-8458-f4060ddd58c7
@jongio
jongio merged commit c5cc6e1 into main Aug 5, 2026
4 checks passed
@jongio
jongio deleted the fix/ai-mcp-hardening branch August 5, 2026 01:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant