Skip to content
Case studies6 min read

GitHub Copilot CamoLeak: Source Code Exfiltration Explained

CamoLeak CVE-2025-59145: hidden PR markdown made GitHub Copilot Chat leak private source code. How deny-by-default AI governance limits the blast radius.

In October 2025 the security firm Legit Security disclosed a vulnerability in GitHub Copilot Chat that it named CamoLeak, rated at a CVSS score of about 9.6. The technique planted instructions inside a pull request or issue using markdown that renders invisibly to a human reader. When a developer asked Copilot Chat about that pull request, the assistant read the hidden text as if it were a legitimate command (Legit Security).

The planted instructions told Copilot to read secrets and private source code from the context it had access to, then to exfiltrate that data. The exfiltration channel was the clever part. GitHub serves user-supplied images through its own Camo proxy, which sits inside the platform's content security policy. The attack encoded stolen data into the ordering of requests to roughly 100 attacker- controlled images served through Camo, turning an image-rendering feature into a covert outbound channel that the CSP did not block (Dark Reading).

GitHub disabled image rendering in Copilot Chat in August 2025, before the public disclosure in October (The Register). The fix closed the specific channel. The shape of the problem is general: an assistant treated untrusted repository content as trusted instruction, and it held the ability to both read sensitive material and reach the outside world.

What actually failed: the governance gap

CamoLeak combined two failures that compound each other. The first is that Copilot Chat parsed attacker-authored content from a pull request as a directive. That is the nature of indirect prompt injection, and no permission model prevents a language model from reading text in its context.

The second failure is the one a control plane addresses. The assistant could read secrets and private source, and it could cause outbound requests, in this case through the Camo image channel. Read access and egress sat in the same agent with no boundary between them. Once injection supplied the intent, nothing stood between the model deciding to leak and the leak actually leaving the network. The dangerous capability was simply present and ungoverned.

How MakerChecker changes the outcome

MakerChecker is an agent control plane: authority lives outside the model and is enforced at runtime, so the question is not what the model wants to do but what it is permitted to do.

Model the read-answer assistant as a role under deny-by-default. It starts with zero capability. It is granted only the skills it genuinely needs to answer questions about code. Any outbound fetch is a separate skill, and that skill is never granted to this role. When injected text drives the agent toward an external request, the call has no grant behind it. The control plane denies it and records the denial.

role: copilot-read-assistant
grants:
  - skill: repo.read@1          # tier: standard
  - skill: answer.compose@1     # tier: standard
  # outbound.fetch  -> NOT GRANTED (egress has no skill)
  # secrets.read    -> NOT GRANTED (scoped out of this role)

Least privilege does the second half of the work. Limiting which secrets and repositories the assistant can read shrinks what any injection is able to ask for. A prompt cannot exfiltrate a credential the agent was never allowed to load. If a workflow does require touching a sensitive store, that read is modeled as a higher-tier skill behind an approval gate, so a named human signs before it runs rather than an injected comment triggering it silently.

Every step lands in a tamper-evident audit log. The untrusted-driven secret read, the attempted outbound emission, and the denial are all written into an append-only ledger that is hash-chained and signed with Ed25519. The record can be verified offline against a published spec by someone with no access to your systems. After an incident you can show exactly what the agent reached for, what it was refused, and when, instead of reconstructing intent from application logs.

In the runnable scenario, the role attempts secrets.read and then the outbound image fetches. The secret read is scoped down or denied, the egress has no granted skill, and both attempts are recorded. The injection still fires. The exfiltration does not complete, because the capability it needed was never the agent's to use.

What MakerChecker would not fix

This is worth stating plainly. MakerChecker does not stop Copilot from parsing the hidden comment. The model still reads the attacker's instructions, and the product does not make the model smarter or filter content. It also does not fix the Camo CSP bypass itself, which was a platform-level rendering issue that GitHub addressed by disabling image rendering.

Deny-by-default helps most precisely when egress is something you can leave ungranted. If a legitimate workflow needs the same outbound capability the attacker abused, the control plane forces that path through a gate rather than removing it, which raises the bar but does not make the surface disappear. The honest claim is narrow: separating read from egress and granting nothing by default shrinks the blast radius of an injection and leaves a verifiable record. It does not prevent the model from being deceived.

See the configuration: examples/rogue-ai/camoleak-github-copilot-chat-source-code-exfiltration

Frequently asked

What is CamoLeak (CVE-2025-59145)?
CamoLeak is a critical vulnerability (CVSS ~9.6) in GitHub Copilot Chat disclosed by Legit Security in October 2025. Attackers planted invisible instructions in pull request comments using hidden markdown. When a developer asked Copilot Chat about the PR, the assistant read those instructions as legitimate commands and exfiltrated private source code and secrets through GitHub's Camo image proxy.
How did the CamoLeak exfiltration channel work?
Stolen data was encoded into the ordering of HTTP requests to roughly 100 attacker-controlled images served through GitHub's Camo proxy. Because Camo sits inside GitHub's own content security policy, the outbound channel was not blocked. GitHub fixed the specific vector by disabling image rendering in Copilot Chat in August 2025.
Would AI governance controls have prevented CamoLeak?
Deny-by-default permissions would not stop Copilot from parsing the hidden instructions, but they would stop the exfiltration from completing. If the assistant role has no granted outbound-fetch skill, the control plane denies any external request and records the attempt. Separating read access from egress capability shrinks the blast radius even when the model is deceived.

Where this goes to work

How MakerChecker works — the six primitives

Agents as employees, versioned grants, structural segregation of duties, approval gates, role limits, and a signed audit a regulator verifies offline.

See it for yourself

See an agent get stopped.

One command starts the demo: an agent stopped from signing off its own work, and the signed evidence file an inspector can check for themselves.

Designed against the rules your auditors already enforce.