Reproducing a critical CVE that had no public exploit


A critical GitLab vulnerability was disclosed a few days ago. Unauthenticated, remotely exploitable, rated 9.4 out of 10 — the kind that lets an attacker modify or delete projects they should never be able to touch. Attackers were exploiting it in the wild within two days.

There was no public proof-of-concept. Nobody had published how to actually do it.

That is the interesting version of security work. Anyone can download an exploit and run it. Building one yourself, from nothing, is where you actually learn something. So I did — in an isolated lab, against my own deliberately vulnerable copy, with a fake secret planted inside as bait.

Here is what happened, including the ending, which is not the ending I wanted.

The patch is the map

When a vendor fixes a security hole, the fix itself quietly gives the bug away. The code before the patch and the code after it differ in exactly one place — and that place is the weakness. It is like watching which lock a locksmith replaces.

So the first move was not to attack anything. It was to download the vulnerable version of the source and the fixed version, and compare them. Four small files had changed. Three were noise. One was the fix.

The vulnerable code stored some bookkeeping in a shared place — a single notepad that every request wrote to. The fix replaced that shared notepad with a separate page for each request. That one change tells you the entire bug: requests were scribbling on each other’s notepad.

What the bug actually is

GitLab has a feature that lets a client ask for parts of a response that do not exist yet in its version. The server strips those “future” parts out, runs the request, and returns nothing for them. Useful, harmless-looking plumbing.

The flaw was in how it tracked that stripping. It kept the original, un-stripped request in that shared notepad. And when it came time to actually run a request, it would grab whatever was on the notepad — which might have been written by a different request that came through at the same time.

The consequence: one request could end up executing another request’s instructions.

Proving it

I stood up a vulnerable GitLab in a sealed-off lab and started testing. The cleanest way to catch the bug in the act is to send two requests together — one asking for something harmless labelled VICTIM, the other labelled ATTACKER — and see whose instructions come back.

They came back crossed. The VICTIM slot returned the ATTACKER’s data. The notepad had been overwritten, exactly as the patch predicted.

Then I hammered it. Twenty in a row, then ten at once. Every single one contaminated. This was not a flaky race condition that fires one time in fifty — it was completely reliable. The mechanism was real and I could reproduce it on demand.

I even found something the official advisory does not mention: I could make a request that was supposed to only read data secretly run a command that changes things. Those two categories are meant to be strictly separated inside GitLab. The bug let me blur the line. That was a genuinely new observation.

And then it stopped

Here is where it gets honest.

Contaminating requests is the mechanism. It is not yet damage. To prove the vulnerability’s headline — an unauthenticated stranger stealing or destroying private data — I had to actually make that happen. So I planted a private project with a secret inside it, confirmed that an anonymous visitor could not read it normally, and then threw every version of the attack at it.

The secret never leaked.

Every attempt to read the private data came back empty. Every attempt to change something was refused. It turned out GitLab checks who you are at the very last moment, right before it does anything real — and my attack could swap the instructions but not swap who I was pretending to be. The contamination was happening in my own anonymous session, so it only ever had my own (zero) permissions.

I did not give up there. The most likely way the full attack works is if the contamination leaks not just between requests in one batch, but between entirely separate people’s requests — an attacker’s instructions landing inside a logged-in victim’s session. I reconfigured the server into the exact arrangement most likely to allow that, ran a logged-in victim reading their own private data while an anonymous attacker flooded the server in parallel, and watched for any crossover.

Nearly six hundred overlapping requests. Zero crossovers. In this setup, the leak simply does not cross between people.

Why the wall is the point

I could have stopped after “twenty out of twenty contaminated” and written the triumphant version: critical vulnerability reproduced. It would have been a lie. What I actually have is this: the bug is real and I can trigger it perfectly, but in my lab I could not turn it into the theft or destruction the advisory warns about. The gap between “the mechanism works” and “I stole something” is a real, unsolved question.

That honesty is not a consolation prize. It is the whole job.

The difference between someone who runs downloaded exploits and someone who does security work is not who can make a tool spit out “VULNERABLE.” It is who knows exactly what they have proven and what they have not. Overclaiming a finding is how you burn your credibility with the one audience that matters — the engineers who have to act on what you tell them. “Reliable mechanism, no demonstrated impact in this configuration, here are the three conditions I could not test” is worth more than a confident wrong answer, every time.

So that is where I stopped. Not because the investigation is finished — it is not — but because that is the exact line where the evidence ran out, and pretending otherwise would undo the entire point of doing it myself.

What I took from it

You do not need a published exploit to learn a vulnerability. The patch hands you the map, the bug is usually simpler than the scary headline suggests, and a sealed lab with a planted secret turns “I think this is bad” into a yes-or-no experiment.

And the most valuable output of a day like this is not the exploit. It is a precise account of what is true, what is not, and where the boundary sits — written so that the next person does not have to trust me, only check me.