TL;DR: This one was almost offensively simple. Windmill’s NativeTS executor dropped workspace environment variable values into single-quoted JavaScript without escaping a single quote, which meant a workspace admin could inject code into every NativeTS run in that workspace. The public advisory is marked Low, but NVD lists CVE-2026-33881 with a CNA CVSS 4.0 score of 7.3 High, which is exactly the kind of severity split that makes this stuff interesting.
The bug, straight up
I found a code injection issue in Windmill’s NativeTS executor that came down to one very basic mistake.
A workspace environment variable value was interpolated directly into a JavaScript string literal. No escaping for single quotes. That meant I could put a
That is the whole trick. No weird parser confusion. No race. No sandbox escape. Just unsafe string construction in the wrong place.
What made it interesting
At first glance, this looks like one of those bugs people are tempted to downplay because the attacker needs to be a workspace admin. The advisory itself even frames it that way.
I do not think that tells the full story.
The injected code does not run as the admin who plants the environment variable. It runs in the context of whoever executes the NativeTS script. That is a completely different security boundary. Once I confirmed that, the bug stopped looking like a niche admin footgun and started looking like a clean privilege-crossing primitive.
Where it was happening
The vulnerable code path built a JavaScript preamble from reserved variables and environment variables. It looked like this:
That
So if
Workspace environment variables were merged into those reserved variables, and they accepted arbitrary string values. So the path was nice and direct. Set a workspace env var, wait for someone to run any NativeTS script, enjoy your code execution.
The first proof was trivial
For the first pass, I did not bother being fancy. I set a workspace environment variable to this:
Then I created the most boring NativeTS script possible:
The script itself returned
Which is exactly what I wanted to see. The payload from the environment variable executed inside the script runtime even though the script body had nothing to do with it.
That is always a satisfying moment. The bug is no longer theoretical. It is now a thing with a log line.
The part that mattered
The more serious payload was obvious pretty quickly.
The preamble exposed reserved variables like
This worked:
That gave me the executing user’s JWT and email in the logs. Not the admin who configured the environment variable. The user who ran the script.
That is the part that makes the “it only needs workspace admin” argument feel a bit too casual. If one role can silently plant code that executes as another role and steal their API token, that is not just a configuration abuse issue. That is a real trust boundary problem.
Why it worked
The mechanic here is simple enough that it is worth stating plainly.
Windmill was building JavaScript source code by hand. It treated attacker-controlled input as though it were safe to embed between quotes. It was not.
Once a single quote appears in the value, the generated code changes shape:
That is not a malformed value anymore. That is valid JavaScript with an extra statement in the middle.
And because this happens in the preamble, the payload runs before the script logic the user actually wrote. So the attacker gets execution in a place most users will never think to inspect.
Where it can go from there
Once I had code execution in that context, the rest was not hard to reason about.
I could steal outputs, tamper with return values, and exfiltrate whatever the script had access to. I could also read files from the worker filesystem. On unsandboxed deployments, which the advisory notes are the default, that can get uglier because worker secrets may become reachable through places like
The fix was exactly what you would expect
This is one of those bugs where the patch is not glamorous.
Before:
After:
That is it. Escape backslashes. Escape single quotes. Stop turning input into code by accident.
Windmill fixed it in
The disclosure process was refreshingly painless
This was also my first actually assigned CVE and my first published CVE, so I expected at least some friction somewhere in the process.
Instead, it was smooth to the point of being almost suspicious.
I reported it on March 24, 2026. I got a response the same day. Roughly half a day later, a CVE had already been assigned. The GitHub security advisory went public on March 25, 2026, and NVD lists CVE-2026-33881 as published on March 27, 2026. For a first disclosure, that is about as clean as it gets.
Ruben Fiszel was great to work with. Fast, clear, no nonsense, no pointless dragging of feet. That should not be rare, but it still stands out when it happens.
The weird severity split
The part I like most about this case is the public disagreement in how it reads at a glance.
The GitHub advisory page labels it Low. NVD shows the CNA score at 7.3 High. Both of those are now attached to the same bug in public.
Personally, I think the High score tells the more honest story.
Yes, the attacker needs workspace admin. But the payload executes as other users. It can steal their tokens. It can impersonate them through the API. In the wrong deployment, it can likely reach much more. Calling that Low feels a bit too relaxed for a bug that lets one trusted role quietly parasitize every NativeTS execution in the workspace.
Wrap up
I like bugs like this because they are not magic. They are just bad boundaries made visible.
A single unescaped quote in the wrong code generator turned a workspace setting into a code execution primitive. From there, token theft and lateral abuse were not some exotic second stage. They were the natural consequence.
Also, I will admit it, seeing my first CVE go from report to public record this cleanly felt good.
Not bad for one quote.
The bug, straight up
I found a code injection issue in Windmill’s NativeTS executor that came down to one very basic mistake.
A workspace environment variable value was interpolated directly into a JavaScript string literal. No escaping for single quotes. That meant I could put a
' in the value, break out of the string, and run arbitrary JavaScript inside every NativeTS script executed in that workspace.That is the whole trick. No weird parser confusion. No race. No sandbox escape. Just unsafe string construction in the wrong place.
What made it interesting
At first glance, this looks like one of those bugs people are tempted to downplay because the attacker needs to be a workspace admin. The advisory itself even frames it that way.
I do not think that tells the full story.
The injected code does not run as the admin who plants the environment variable. It runs in the context of whoever executes the NativeTS script. That is a completely different security boundary. Once I confirmed that, the bug stopped looking like a niche admin footgun and started looking like a clean privilege-crossing primitive.
Where it was happening
The vulnerable code path built a JavaScript preamble from reserved variables and environment variables. It looked like this:
Code:
reserved_variables
.iter()
.map(|(k, v)| format!("const {} = '{}';\nprocess.env['{}'] = '{}';\n", k, v, k, v))
.collect::<Vec<String>>()
.join("\n")
That
v goes straight into a single-quoted JavaScript string literal. No escaping.So if
v contains a single quote, the generated code stops being data and starts being syntax. At that point you are no longer setting an environment variable. You are writing JavaScript into the executor preamble.Workspace environment variables were merged into those reserved variables, and they accepted arbitrary string values. So the path was nice and direct. Set a workspace env var, wait for someone to run any NativeTS script, enjoy your code execution.
The first proof was trivial
For the first pass, I did not bother being fancy. I set a workspace environment variable to this:
Code:
'; console.log("CODE_INJECTION_WORKS"); '
Then I created the most boring NativeTS script possible:
Code:
export async function main() {
return "hello";
}
The script itself returned
hello. But the logs also printed:
Code:
--- FETCH TS EXECUTION ---
CODE_INJECTION_WORKS
CODE_INJECTION_WORKS
Which is exactly what I wanted to see. The payload from the environment variable executed inside the script runtime even though the script body had nothing to do with it.
That is always a satisfying moment. The bug is no longer theoretical. It is now a thing with a log line.
The part that mattered
The more serious payload was obvious pretty quickly.
The preamble exposed reserved variables like
WM_TOKEN, WM_EMAIL, and WM_WORKSPACE as constants in the same execution context. So the injected code could just read them directly.This worked:
Code:
'; console.log('STOLEN_TOKEN=' + WM_TOKEN); console.log('STOLEN_EMAIL=' + WM_EMAIL); '
That gave me the executing user’s JWT and email in the logs. Not the admin who configured the environment variable. The user who ran the script.
That is the part that makes the “it only needs workspace admin” argument feel a bit too casual. If one role can silently plant code that executes as another role and steal their API token, that is not just a configuration abuse issue. That is a real trust boundary problem.
Why it worked
The mechanic here is simple enough that it is worth stating plainly.
Windmill was building JavaScript source code by hand. It treated attacker-controlled input as though it were safe to embed between quotes. It was not.
Once a single quote appears in the value, the generated code changes shape:
Code:
const INJECTED_VAR = ''; console.log("owned"); '';
That is not a malformed value anymore. That is valid JavaScript with an extra statement in the middle.
And because this happens in the preamble, the payload runs before the script logic the user actually wrote. So the attacker gets execution in a place most users will never think to inspect.
Where it can go from there
Once I had code execution in that context, the rest was not hard to reason about.
I could steal outputs, tamper with return values, and exfiltrate whatever the script had access to. I could also read files from the worker filesystem. On unsandboxed deployments, which the advisory notes are the default, that can get uglier because worker secrets may become reachable through places like
/proc/1/environ. The original bug was just quote escaping. The practical impact goes a lot further than that.The fix was exactly what you would expect
This is one of those bugs where the patch is not glamorous.
Before:
Code:
format!("const {} = '{}';\n", k, v)
After:
Code:
format!("const {} = '{}';\n", k, v.replace('\', "\\").replace(''', "\'"))
That is it. Escape backslashes. Escape single quotes. Stop turning input into code by accident.
Windmill fixed it in
1.664.0, with versions before that affected according to the advisory.The disclosure process was refreshingly painless
This was also my first actually assigned CVE and my first published CVE, so I expected at least some friction somewhere in the process.
Instead, it was smooth to the point of being almost suspicious.
I reported it on March 24, 2026. I got a response the same day. Roughly half a day later, a CVE had already been assigned. The GitHub security advisory went public on March 25, 2026, and NVD lists CVE-2026-33881 as published on March 27, 2026. For a first disclosure, that is about as clean as it gets.
Ruben Fiszel was great to work with. Fast, clear, no nonsense, no pointless dragging of feet. That should not be rare, but it still stands out when it happens.
The weird severity split
The part I like most about this case is the public disagreement in how it reads at a glance.
The GitHub advisory page labels it Low. NVD shows the CNA score at 7.3 High. Both of those are now attached to the same bug in public.
Personally, I think the High score tells the more honest story.
Yes, the attacker needs workspace admin. But the payload executes as other users. It can steal their tokens. It can impersonate them through the API. In the wrong deployment, it can likely reach much more. Calling that Low feels a bit too relaxed for a bug that lets one trusted role quietly parasitize every NativeTS execution in the workspace.
Wrap up
I like bugs like this because they are not magic. They are just bad boundaries made visible.
A single unescaped quote in the wrong code generator turned a workspace setting into a code execution primitive. From there, token theft and lateral abuse were not some exotic second stage. They were the natural consequence.
Also, I will admit it, seeing my first CVE go from report to public record this cleanly felt good.
Not bad for one quote.