Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lack of Anti-CSRF Protection in OpenWRT – /ubus Accepts Unauthorized Requests #7635

Open
1 task done
dankra001 opened this issue Feb 16, 2025 · 2 comments
Open
1 task done

Comments

@dankra001
Copy link

Is there an existing issue for this?

  • I have searched among all existing issues (including closed issues)

screenshots or captures

Image

Image

Image

Image

Actual behaviour

During security testing with OWASP ZAP, an alert was triggered indicating Absence of Anti-CSRF Tokens in OpenWRT's /ubus API.
The issue was classified as Risk: Medium with Confidence: Low by OWASP ZAP.

Upon further manual verification, it was confirmed that no CSRF protection mechanism is enforced, allowing API calls to be executed with just a valid sessionid.

  • In OpenWRT's JavaScript frontend (L.env), both sessionid and token are present.
  • However, the token is never sent in any POST requests, meaning the API does not check for CSRF protection.
  • Requests to /ubus are processed successfully as long as the attacker possesses the sessionid.

Proof of Concept (PoC)

I successfully executed the following PowerShell script CSRF TEST:

$body = '[{"jsonrpc": "2.0", "id": 11, "method": "call", "params": ["SESSION_ID", "file", "write", {"path": "/etc/sysupgrade.conf", "data": "## CSRF TEST\n"}]}]'

Invoke-WebRequest -Uri "https://openwrt.home/ubus" `
  -Method POST `
  -Headers @{"Content-Type"="application/json"} `
  -Body $body

🔹 Note: Replace SESSION_ID in the script above with the actual value of sessionid, which can be found in OpenWRT’s JavaScript frontend (L.env) or extracted from the sysauth_https session cookie in the browser.

As a result, the /etc/sysupgrade.conf file was modified, confirming that no CSRF validation occurs.

The tests were conducted both with and without an Nginx proxy, and the results remained the same.
Regardless of whether requests were sent directly to OpenWRT or proxied through Nginx, the API did not enforce CSRF protection.

If I misunderstood anything about the authentication or security model, please feel free to correct me. I appreciate any feedback!

Expected behaviour

  • The /ubus API should require a CSRF token to prevent unauthorized actions.
  • Requests without a valid CSRF token should be rejected with Access Denied.
  • sessionid should not be enough to execute API requests without additional verification.

Steps to reproduce

1️⃣ Log into OpenWRT Web UI.
2️⃣ Open Developer Tools (F12) and locate sessionid using one of the following methods:

  • Option 1: Elements Tab – Search for sessionid inside the JavaScript object L = new LuCI({...}).
  • Option 2: Console Tab – Enter the following command:
    console.log(L.env);
    This will print the LuCI object where sessionid and token are defined.

3️⃣ Execute the PowerShell script above, replacing SESSION_ID with the extracted value.
4️⃣ The request succeeds, modifying the router’s configuration with ## CSRF TEST.

Additional Information

🔹 Tested on OpenWRT Versions:

OpenWrt 23.05.4 r24012-d8dd03c46f / LuCI openwrt-23.05 branch git-24.332.79522-a493155
OpenWrt 24.10.0 r28427-6df0e3d02a / LuCI openwrt-24.10 branch 25.014.55016~7046a1c

🔹 Backend Analysis:

The CSRF token is generated in dispatcher.uc by session_setup() using randomid(16).
However, it is never validated in /ubus requests, likely due to the test_post_security() function not being invoked.

🔹 Potential Impact:

If an attacker steals sessionid (via XSS, MITM, or session hijacking), they can execute arbitrary API requests.
Lack of CSRF validation allows full router compromise if sessionid is leaked.

What browsers do you see the problem on?

Firefox, Chrome, Microsoft Edge

Relevant log output

@stokito
Copy link
Contributor

stokito commented Feb 23, 2025

The CSRF would be difficult to add, especially given that requests order may be different, and may break many things. The LUCI admin panel usually is accessed only from LAN so if an attacker stole the sessionId it also needs to get into a local network.
The sessionId may be attached to an IP as an additional protection. But this also may broke usage of the LUCI from a mobile internet e.g. when you are in a train.
Given that keeping functionality is very important and you don't want to loos an access to a router it would be better to keep it as is.
Instead we may try to implement Refresh Tokens as in OAuth i.e. to have a short living sessionId and recreate a new one with a long standing Refresh Token. Still, this is more "nice to have" thing.

@jow-
Copy link
Contributor

jow- commented Mar 7, 2025

Also note that /ubus is not a LuCI specific backend but rather a generic HTTP API provided by OpenWrt. Furthermore, the ubus API does not accept Cookies for authentication, rather a valid session ID has to be passed in-line as part of the request body. There is a fallback /admin/ubus route which is implemented as LuCI controller, that one falls back to the current authentication session (established by the sysauth cookie) if no in-line sessionid is provided (or rather if it is zeroed).

So in conclusion:

According to my understanding, CSRF protection generally should be employed for endpoints that receive authenticated requests where the browser will automatically send authentication credentials along with it, so primarily basic-auth and cookie protected endpoints. Considering this, it should be sufficient to drop the fallback to cookie provided session id in order to address this weakness:

diff --git a/modules/luci-base/ucode/controller/admin/index.uc b/modules/luci-base/ucode/controller/admin/index.uc
index d433e1161e..471be0aaf6 100644
--- a/modules/luci-base/ucode/controller/admin/index.uc
+++ b/modules/luci-base/ucode/controller/admin/index.uc
@@ -56,9 +56,6 @@ function ubus_request(req) {
                if (type(arg) != 'object' || exists(arg, 'ubus_rpc_session'))
                        return ubus_reply(req.id, null, -32602, 'Invalid parameters');
 
-               if (sid == '00000000000000000000000000000000' && ctx.authsession)
-                       sid = ctx.authsession;
-
                if (!ubus_access(sid, obj, fun))
                        return ubus_reply(req.id, null, -32002, 'Access denied');
 

After that, malicious actors would need to extract the proper session ID from the page source, which would require an XSS vulnerability or some other kind of exfiltration vector. If such a vulnerability is present, the malicious actor could also extract the proper CSRF token.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants