The YOURLS API allows JSONP responses via a `callback` parameter. Versions prior to 1.10.3 failed to sanitize this parameter, allowing attackers to inject JavaScript payloads directly into the API response. This results in Reflected XSS, potentially compromising admin accounts.
A deep dive into a Reflected Cross-Site Scripting (XSS) vulnerability in the popular YOURLS URL shortener. By exploiting legacy JSONP implementations in the API, attackers can execute arbitrary JavaScript in the context of the administrator's session.
JSONP (JSON with Padding) is like that one guest at a party who overstays their welcome by about 15 years. Back in the dark ages before Cross-Origin Resource Sharing (CORS) was standardized, developers needed a way to fetch data from different domains. The hacky solution? <script> tags.
Since script tags aren't bound by the Same-Origin Policy, you could load a script from api.example.com. But to make use of the data, the API had to wrap the JSON response in a function call that defined on your page. Hence, callback=myFunction becomes myFunction({...}).
YOURLS (Your Own URL Shortener), a fantastic piece of self-hosted PHP software, supports this feature in its API. Unfortunately, handling JSONP correctly is notoriously difficult. If you blindly trust the function name provided by the user, you aren't just serving data; you're serving an execution context. And that is exactly what happened here.
The vulnerability lived in includes/functions-api.php. The logic was deceptively simple: check if the user wants JSONP, grab the callback name, and slap it onto the front of the JSON output.
Here is the logic found in the vulnerable versions:
// The "Before" code
$callback = isset( $output['callback'] ) ? $output['callback'] : '';
// ... later in the output logic ...
if ( isset( $output['callback'] ) ) {
// Direct concatenation of user input
echo $callback . '(' . json_encode( $output ) . ')';
}Notice the lack of validation? The code assumes $callback is a benign string like renderData. It doesn't check if it contains parentheses, semicolons, or the collected works of Shakespeare written in JavaScript.
This is a textbook reflection sink. The application takes input from the URL ($_REQUEST), processes it zero percent, and echoes it back with a Content-Type that browsers love to execute.
Exploiting this is trivial because there were no filters to bypass. An attacker simply needs to construct a URL that closes the function call syntax and starts a new statement.
In a standard JSONP request, the URL looks like this:
http://yourls-site.com/yourls-api.php?action=stats&format=jsonp&callback=myFunc
The server responds with:
myFunc({"statusCode":200,...})
However, if we change the callback to a payload:
http://yourls-site.com/yourls-api.php?action=stats&format=jsonp&callback=alert(document.cookie);//
The server naively responds with:
alert(document.cookie);//({"statusCode":200,...})
When a victim (specifically an admin) visits this link, the browser executes alert(document.cookie). The trailing // comments out the rest of the legitimate JSON, preventing syntax errors. In a real-world scenario, an attacker wouldn't just pop an alert box; they would fetch document.cookie and send it to a C2 server, stealing the admin session.
The patch, applied in commit b1c6100e0aa6fef58c9c1a394ccc19352c3a480a, introduces the only robust defense against this bug class: Strict Whitelisting.
You cannot sanitize XSS with a blacklist (bad guys always find a new character). You must define exactly what is allowed. The YOURLS team added a new function yourls_validate_jsonp_callback().
function yourls_validate_jsonp_callback( $callback ) {
// Only allow alphanumeric, underscores, dollars, and dots
if ( preg_match( '/[^a-zA-Z0-9_$.]/', $callback ) ) {
return false;
}
// ... checks for unicode escapes ...
return true;
}This Regex /[^a-zA-Z0-9_$.]/ ensures that no parentheses (), semicolons ;, or spaces can pass through. If you can't open a parenthesis, you can't call a function (mostly). They also explicitly block Unicode escapes (like \u0028 for (), closing the door on obscure encoding bypasses.
Why is XSS in a URL shortener dangerous? It's just redirecting links, right?
Wrong. If an attacker compromises a YOURLS instance, they control the traffic flow. They can:
If the YOURLS instance is set to Private (YOURLS_PRIVATE = true), the impact is slightly mitigated because the attacker needs to target an authenticated admin. However, these are often the most valuable targets. If the instance is Public, any user can trigger this reflection, potentially turning the shortener into a launchpad for attacks against the general public.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
YOURLS YOURLS | < 1.10.3 | 1.10.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network (API) |
| CVSS v3 | 8.2 (High) |
| Impact | Session Hijacking, Redirection Manipulation |
| Fix Commit | b1c6100 |
| Exploit Status | PoC Available |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Get the latest CVE analysis reports delivered to your inbox.