A JSONP callback is executable output; constrain it to an identifier before you emit it.
The wrapper around the JSON was never validated.
A JSONP response is JavaScript: the server wraps a JSON payload in a caller-named function invocation and returns it as a script. The JSON body can be encoded safely, but the callback name sits outside that body, and it is the part a browser will execute. The security boundary is therefore the callback name, not the data.
The upstream advisory records a reflected cross-site-scripting issue in the API for versions up to 1.10.2. The endpoint copied a request-supplied callback into the response and concatenated it into the script without constraining it to a function identifier.
From request parameter to executed script.
The API entry point copied either the callback or the jsonp request parameter into the response model with no validation, so any string was retained. The formatter then built the body by placing that string ahead of the JSON-encoded payload, opening a parenthesis, and serving the result as application/javascript.
Because the response is delivered as a script, an attacker page that loads the endpoint through a script element runs the reflected code in the shortener’s origin. The impact is not a harmless dialog: code executes as whatever user visits the attacker page, including a signed-in administrator. Public API mode allows this without authentication.
- Read the callback parameter
Copied into the response as-is.
- Concatenate before the JSON
Placed outside the encoded body.
- Serve as JavaScript
The browser executes the wrapper.
- Read the callback parameter
Taken from the request.
- Validate as an identifier
Reject anything outside the allowed set.
- Emit or refuse
An invalid callback yields an error, not script.
Conceptual control flow reconstructed from the upstream source. It is not a captured request or a TraceMint execution trace.
Validate the callback against an identifier allowlist.
The upstream fix adds a validator that accepts only a safe identifier and returns false otherwise, and the formatter now runs the callback through it. An invalid callback produces a structured error response instead of a script, so a value carrying JavaScript operators can no longer reach the output.
The change also adds unit tests for the formatter. Moving the decision to the point of emission means every caller of the JSONP path inherits the same constraint, rather than relying on each entry point to sanitize its own input.
if( isset( $_REQUEST['callback'] ) ) $return['callback'] = $_REQUEST['callback'];$callback = isset( $output['callback'] ) ? $output['callback'] : '';$result = $callback . '(' . json_encode( $output ) . ')';$callback = isset( $output['callback'] ) ? yourls_validate_jsonp_callback($output['callback'] ) : '';if( $callback === false ) {function yourls_validate_jsonp_callback($callback ) {Selected upstream lines; indentation normalized. Links include surrounding code at immutable revisions.
Public mode makes the reflection unauthenticated.
The repository advisory identifies 1.10.3 as patched and records a user-interaction requirement: a victim must load the attacker’s page. In public API mode the reflection needs no authentication. The linked public records assign a GHSA identifier and no identifier from the reviewed advisory database.
- A public API
- Public mode accepts the callback parameter without authentication.
- A loaded script element
- The victim’s browser fetches the endpoint as JavaScript.
- An origin with a session
- Execution in the shortener’s origin reaches an authenticated administrator.
Read the raw bytes, not the rendered page.
A regression check requests the JSONP endpoint with a callback that contains JavaScript operators and reads the raw response body. A patched build should return an error rather than a body that begins with the supplied operators. Verify the response content type and the leading bytes, since a browser that refuses to run malformed script can hide a reflection that is still present. The scenarios below are suggested review checks, not reported TraceMint test results.
| Scenario | Expected boundary |
|---|---|
| Callback is a plain identifier | The response wraps the JSON in that function name. |
| Callback carries operators | The endpoint returns an error rather than reflecting the value. |
| Callback via the jsonp parameter | The same validation applies to the alternate parameter name. |
| Response content type | A rejected callback does not emit an executable script body. |
Treat response wrappers as code, not decoration.
For reviewers, JSON encoding secures the data object but says nothing about a callback concatenated around it. Any value that becomes part of an executable response—function names, template fragments, header-driven redirects—needs its own validation at the point of emission.
The public advisory credits the TraceMint account as reporter, alongside the maintainers who developed and verified the remediation; the original advisory holds the complete credit record. This study analyzes public records and immutable source; it does not claim sole discovery or access to any deployment.
Follow the evidence.
Public upstream records and immutable code revisions, reviewed 2026-09-22.
- YOURLS security advisory
Impact, API-mode conditions, patched release and complete credits.
- API entry before the fix
Immutable parent revision · ddc41ad1 · callback copied without validation.
- JSONP formatter before the fix
Callback concatenated ahead of the encoded body.
- Upstream corrective commit
b1c6100e · validate callbacks and add formatter tests.
- JSONP formatter after the fix
Callback validated before it is emitted.
- Callback validator
Accepts only a safe identifier; returns false otherwise.
Brief code excerpts from YOURLS/YOURLS. Upstream license: MIT License. All remediation described here is upstream work.
Read the original record
This case study analyzes an already-public advisory and upstream source. For the complete impact, remediation and credit record, see the YOURLS security advisory.
Editorial publication is separate from disclosure. No private discovery timeline or method has been inferred.