Page MenuHomePhabricator

CVE-2026-34095: action=raw with Special:Mypage subpage title responds with "Content-Type: text/html" on ctype=text/javascript request
Closed, ResolvedPublicSecurity

Description

Steps to replicate the issue

https://commons.wikimedia.org/w/index.php?title=Special:MyPage/common.js&action=raw&ctype=text/javascript

What happens?:

The source code is rendered by the browser as arbitrary HTML.

What should have happened instead?:

Served as plain text like this URL does, for the same content:

https://commons.wikimedia.org/w/index.php?title=User:Krinkle/common.js&action=raw&ctype=text/javascript

We go through great lengths in MediaWiki to make sure our endpoints do not allow serving of arbitrary HTML. For example we set X-Content-Type-Options: nosniff, and we carefully escape any free-form text in api.php and rest.php responses that use JSON format to ensure no HTML-like tags are served as-is but rather use redundant slash escaping to prevent looking like HTML.

Other information

During today's worm accident (T419137), there was briefly talk in the #technical channel unofficial Discord about how to workaround the temporary user script disablement by using a Tampermonkey (GreaseMonkey) script. User:Sportzpikachu shared an approach that involved fetching https://en.wikipedia.org/w/index.php?title=Special:MyPage/common.js&action=raw&ctype=text/javascript and eval'ing its contents.

I would have thought such title is invalid for this entrypoint, but to my surprise it worked. And moreover, I noticed we respond with a text/html content type instead of the requested text/javascript. The workaround wasn't affected by that bug, because it didn't rely on the content type header and treated the response as plain text.

See also:

Event Timeline

Krinkle set Security to Software security bug.Mar 6 2026, 1:42 AM
Krinkle added projects: Security, Security-Team.
Krinkle changed the visibility from "Public (No Login Required)" to "Custom Policy".
Krinkle changed the subtype of this task from "Feature Request" to "Security Issue".

Isolated example:

capture.png (676×980 px, 29 KB)

On the one hand, exploit seems trivial because you only need to send someone a link to a seemingly-trustworthy domain where they are logged-in and you can do anything with their session and with arbitrary JavaScript.

On the other hand, it requires the payload to exist under their User-page (receiver, not attacker). This makes it non-trivial to mount. But, someone may have all sorts of junk stored under their User-page, and given that those revisions were never intended to be interpreted as raw HTML, one could find or match potential victims. And, it could be combined with social engineering where you indirectly convince or cause someone to store something that will seem innocent on its own.

Seems like this works on non-js pages as well, so anyone can just edit the target's user space to add something evil and then trick them into clicking the link.

For example: https://test.wikipedia.org/w/index.php?title=Special:Mypage/test&action=raw returns a text/html mime type for the raw text of that page

Krinkle triaged this task as High priority.EditedMar 6 2026, 2:43 AM

Aye, that makes it urgent, because while JS subpages are protected (owner and admins), regular subpages are freely editable.

This is effectively the same bug as T235047: [Spike: 4 hours] RedirectSpecialPage not setting block cookies after redirect / {T320363}. ActionEntryPoint resolves the special page redirect, and creates a DerivativeRequest which is used in the Context passed to the action class. DerivativeRequest inherits FauxRequest and does not override response(), so the content-type headers and others set in RawAction have no effect.

Fixing this gets a bit complicated:

  • My first thought was to override DerivativeRequest::response() to either return $this->base->response() (so the original object) or to add a method to allow a caller to explicitely override the response object. However, DerivativeRequest extends FauxRequest, and FauxRequest::response() is type-hinted as FauxResponse. Subclasses are not allowed to widen the return type, so DerivativeRequest::response() cannot return anything but a FauxResponse.
  • So, at least for the initial security patch, we need to copy the headers from the faux response to the real one. Here's one that does it for the actions entry point only, although as the other tasks linked above indicate this is surely not the only place where it'll be required.

See also T419273: Limit the forwarding actions for Special:Random although you'd need a wiki to be extremely tiny for that to be a useful attack vector.

This is effectively the same bug as T235047: [Spike: 4 hours] RedirectSpecialPage not setting block cookies after redirect / {T320363}. ActionEntryPoint resolves the special page redirect, and creates a DerivativeRequest which is used in the Context passed to the action class. DerivativeRequest inherits FauxRequest and does not override response(), so the content-type headers and others set in RawAction have no effect.
...

This works for me testing locally within a basic mw-docker setup. I think this should be ready for deployment during next Monday's (2026-03-23) security deployment window.

Mstyles subscribed.

This is effectively the same bug as T235047: [Spike: 4 hours] RedirectSpecialPage not setting block cookies after redirect / {T320363}. ActionEntryPoint resolves the special page redirect, and creates a DerivativeRequest which is used in the Context passed to the action class. DerivativeRequest inherits FauxRequest and does not override response(), so the content-type headers and others set in RawAction have no effect.

Fixing this gets a bit complicated:

  • My first thought was to override DerivativeRequest::response() to either return $this->base->response() (so the original object) or to add a method to allow a caller to explicitely override the response object. However, DerivativeRequest extends FauxRequest, and FauxRequest::response() is type-hinted as FauxResponse. Subclasses are not allowed to widen the return type, so DerivativeRequest::response() cannot return anything but a FauxResponse.
  • So, at least for the initial security patch, we need to copy the headers from the faux response to the real one. Here's one that does it for the actions entry point only, although as the other tasks linked above indicate this is surely not the only place where it'll be required.

Deployed

Mstyles added a parent task: Restricted Task.Mon, Mar 23, 9:58 PM
Reedy renamed this task from action=raw with Special:Mypage subpage title responds with "Content-Type: text/html" on ctype=text/javascript request to CVE-2026-34095: action=raw with Special:Mypage subpage title responds with "Content-Type: text/html" on ctype=text/javascript request.Wed, Mar 25, 6:20 PM
Reedy added a subscriber: gerritbot.
Reedy reassigned this task from sbassett to taavi.
Reedy added a subscriber: sbassett.

sbassett added a subscriber: matmarex.

+1 to the patch :)

Change #1265642 had a related patch set uploaded (by Reedy; author: Majavah):

[mediawiki/core@master] SECURITY: Actions: Make headers set after redirect actually apply

https://gerrit.wikimedia.org/r/1265642

Change #1265657 had a related patch set uploaded (by Reedy; author: Majavah):

[mediawiki/core@REL1_45] SECURITY: Actions: Make headers set after redirect actually apply

https://gerrit.wikimedia.org/r/1265657

Change #1265663 had a related patch set uploaded (by Reedy; author: Majavah):

[mediawiki/core@REL1_44] SECURITY: Actions: Make headers set after redirect actually apply

https://gerrit.wikimedia.org/r/1265663

Change #1265669 had a related patch set uploaded (by Reedy; author: Majavah):

[mediawiki/core@REL1_43] SECURITY: Actions: Make headers set after redirect actually apply

https://gerrit.wikimedia.org/r/1265669

Change #1265642 merged by jenkins-bot:

[mediawiki/core@master] SECURITY: Actions: Make headers set after redirect actually apply

https://gerrit.wikimedia.org/r/1265642

Change #1265657 merged by jenkins-bot:

[mediawiki/core@REL1_45] SECURITY: Actions: Make headers set after redirect actually apply

https://gerrit.wikimedia.org/r/1265657

Change #1265663 merged by jenkins-bot:

[mediawiki/core@REL1_44] SECURITY: Actions: Make headers set after redirect actually apply

https://gerrit.wikimedia.org/r/1265663

Change #1265669 merged by jenkins-bot:

[mediawiki/core@REL1_43] SECURITY: Actions: Make headers set after redirect actually apply

https://gerrit.wikimedia.org/r/1265669

Where did the $request = $context->getRequest(); line appear to the patches in Gerrit? That is unfortunately shadowing the $request variable from earlier in the function, turning the entire mitigation into a no-op.

Here is a fix for that variable shadowing issue:

Where did the $request = $context->getRequest(); line appear to the patches in Gerrit? That is unfortunately shadowing the $request variable from earlier in the function, turning the entire mitigation into a no-op.

Sorry about that, I missed that when I was fixing phan (via the gerrit web editor, which was probably not the best idea)

Here is a fix for that variable shadowing issue:

+1, works fine for me

Where did the $request = $context->getRequest(); line appear to the patches in Gerrit? That is unfortunately shadowing the $request variable from earlier in the function, turning the entire mitigation into a no-op.

Looks like this issue made it into master and all of the release branch backports. So we probably just need to push your follow-up patch to gerrit, merge it and backport it to same release branches. Not sure if this merits a follow-up email or not; would leave that up to @Reedy's discretion.

Change #1267979 had a related patch set uploaded (by SBassett; author: Majavah):

[mediawiki/core@master] Actions: Fix incorrect variable shadowing

https://gerrit.wikimedia.org/r/1267979

Change #1267983 had a related patch set uploaded (by SBassett; author: Majavah):

[mediawiki/core@REL1_45] Actions: Fix incorrect variable shadowing

https://gerrit.wikimedia.org/r/1267983

Change #1267984 had a related patch set uploaded (by SBassett; author: Majavah):

[mediawiki/core@REL1_43] Actions: Fix incorrect variable shadowing

https://gerrit.wikimedia.org/r/1267984

Change #1267985 had a related patch set uploaded (by SBassett; author: Majavah):

[mediawiki/core@REL1_44] Actions: Fix incorrect variable shadowing

https://gerrit.wikimedia.org/r/1267985

Change #1267979 merged by jenkins-bot:

[mediawiki/core@master] Actions: Fix incorrect variable shadowing

https://gerrit.wikimedia.org/r/1267979

Change #1267985 merged by jenkins-bot:

[mediawiki/core@REL1_44] Actions: Fix incorrect variable shadowing

https://gerrit.wikimedia.org/r/1267985

Change #1267983 merged by jenkins-bot:

[mediawiki/core@REL1_45] Actions: Fix incorrect variable shadowing

https://gerrit.wikimedia.org/r/1267983

Change #1267984 merged by jenkins-bot:

[mediawiki/core@REL1_43] Actions: Fix incorrect variable shadowing

https://gerrit.wikimedia.org/r/1267984

sbassett changed Author Affiliation from N/A to Wikimedia Communities.
sbassett changed the visibility from "Custom Policy" to "Public (No Login Required)".
sbassett changed Risk Rating from N/A to High.
sbassett moved this task from Watching to Our Part Is Done on the Security-Team board.
sbassett changed Author Affiliation from Wikimedia Communities to WMF Technology.

Looks like this issue made it into master and all of the release branch backports. So we probably just need to push your follow-up patch to gerrit, merge it and backport it to same release branches. Not sure if this merits a follow-up email or not; would leave that up to @Reedy's discretion.

IMO it would merit a follow-up email/release, given that the vulnerability being patched here is now public knowledge, but (IIUC) the released fix doesn't actually fix it. (But I suppose YMMV)