Possible RCE execution in StationUrl #54

Closed
opened 2026-08-07 17:39:15 +02:00 by jonbarrow · 5 comments

A few years ago we found a universal RCE path in all NEX clients on the 3DS, Wii U, and Switch. We reported this to Nintendo and they confirmed that they had fixed it already on the NSO servers, but every client still has the vulnerable code

I was only just yesterday given permission to disclose the bug, so I'm alerting relevant parties so everyone can make sure everything is safe, including you guys

TLDR, nn::nex::StationURL::Format in the NEX client has no bounds checks and only allocates enough space for a URL up to length 1024 (some older RDV clients also have this bug, but Quazal/Ubisoft fixed it themselves at some point in the client itself). StationURLs can be submitted by the client to the server and can contain any data (this is by design, since games are allowed to define custom URL parameters outside of the standard ones, which several Ubisoft games do make use of), and the server relays them back to other clients during matchmaking, and there's a code path that calls the Format function when this happens. When we tested this on a 3DS years ago, we got a PoC that was "easy mode" RCE

Nintendo patched this on the server-side by just checking the submitted URL length by the client, which is what we also did at Pretendo Network

I poked around here for a bit and didn't see the same kind of server-side checks, and while you do limit the fields a StationURL can contain, and none of those fields allow for a value of arbitrary length, I believe there is still a path here to trigger the bug

rnex seems to not de-duplicate fields, decoding them into a list rather than a keyed map (or some other method to prevent duplicate fields). This means that a client can send a StationURL string with the same, short, field many times, getting past the 1024 limit

I took out the encoding/decoding methods from rnex to do an isolated test, and got rnex to produce a StationURL longer than 1024 by just repeating the address field 43 times (I chose that field because it was just the longest and so required to least duplication, but this applies to any field). This test was made using the default branch of the repo, if changes are in another branch that fix this then I have not tested those:

fn main() {
	let raw = format!("prudp:/{}", "address=255.255.255.255;".repeat(43));

	let url = StationUrl::try_from(raw.as_str()).expect("decode failed");
	let encoded: String = (&url).into();

	println!("Original input StationURL length: {}", raw.len());
	println!("Encoded StationURL length: {}", encoded.len());
}

Screenshot 2026-08-07 at 11.18.39 AM

I will admit that I am by no means a Rust expert, nor am I well versed in your libraries specifically. I may have made a mistake here or misunderstood something. This is an isolated test with the encoder/decoder, I may have missed some other prevention elsewhere (like if you check the string length in an RMC handler or something), but I didn't see anything like that in my poking. If there are other mitigations for this in place elsewhere, disregard

A few years ago we found a universal RCE path in all NEX clients on the 3DS, Wii U, and Switch. We reported this to Nintendo and they confirmed that they had fixed it already on the NSO servers, but every client still has the vulnerable code I was only just yesterday given permission to disclose the bug, so I'm alerting relevant parties so everyone can make sure everything is safe, including you guys TLDR, `nn::nex::StationURL::Format` in the NEX client has no bounds checks and only allocates enough space for a URL up to length 1024 (some older RDV clients also have this bug, but Quazal/Ubisoft fixed it themselves at some point in the client itself). StationURLs can be submitted by the client to the server and can contain any data (this is by design, since games are allowed to define custom URL parameters outside of the standard ones, which several Ubisoft games do make use of), and the server relays them back to other clients during matchmaking, and there's a code path that calls the `Format` function when this happens. When we tested this on a 3DS years ago, we got a PoC that was "easy mode" RCE Nintendo patched this on the server-side by just checking the submitted URL length by the client, which is what we also did at Pretendo Network I poked around here for a bit and didn't see the same kind of server-side checks, and while you do limit the fields a StationURL can contain, and none of those fields allow for a value of arbitrary length, I believe there is still a path here to trigger the bug rnex seems to not de-duplicate fields, decoding them into a list rather than a keyed map (or some other method to prevent duplicate fields). This means that a client can send a StationURL string with the same, short, field many times, getting past the 1024 limit I took out the encoding/decoding methods from rnex to do an isolated test, and got rnex to produce a StationURL longer than 1024 by just repeating the `address` field 43 times (I chose that field because it was just the longest and so required to least duplication, but this applies to any field). This test was made using the default branch of the repo, if changes are in another branch that fix this then I have not tested those: ```rs fn main() { let raw = format!("prudp:/{}", "address=255.255.255.255;".repeat(43)); let url = StationUrl::try_from(raw.as_str()).expect("decode failed"); let encoded: String = (&url).into(); println!("Original input StationURL length: {}", raw.len()); println!("Encoded StationURL length: {}", encoded.len()); } ``` ![Screenshot 2026-08-07 at 11.18.39 AM](/attachments/e0da7649-03b3-4f1b-8242-141123ab1c3b) I will admit that I am by no means a Rust expert, nor am I well versed in your libraries specifically. I may have made a mistake here or misunderstood something. This is an isolated test with the encoder/decoder, I may have missed some other prevention elsewhere (like if you check the string length in an RMC handler or something), but I didn't see anything like that in my poking. If there are other mitigations for this in place elsewhere, disregard

Thank you for the report, let me just run a check on the refactor branch to see if this is still an issue as we are planning on merging that into main soon

Thank you for the report, let me just run a check on the refactor branch to see if this is still an issue as we are planning on merging that into main soon

Issue should be fixed in dca2900263 for the v0 branch, I have the fix for refactor in my local tree but we haven't deployed that to any production servers and I'm working on fixing some NAT probing issues on it so I won't push that just yet. On my side I ran your code on both refactor and v0 and it seems to be fine now but if you could double check on your side in case I missed something?

Issue should be fixed in [dca2900263](https://git.spbr.net/spacebar/rust-nex/commit/dca2900263511695aee39d6e1da14748ec8f4027) for the `v0` branch, I have the fix for refactor in my local tree but we haven't deployed that to any production servers and I'm working on fixing some NAT probing issues on it so I won't push that just yet. On my side I ran your code on both refactor and v0 and it seems to be fine now but if you could double check on your side in case I missed something?
Author

That should be good enough, that's basically what we/Nintendo did as well. My only suggestion would be to use a proper error code here since a StationURL longer than 1024 is invalid, but the rejection itself handles the fix just fine

That should be good enough, that's basically what we/Nintendo did as well. My only suggestion would be to use a proper error code here since a StationURL longer than 1024 is invalid, but the rejection itself handles the fix just fine

@jonbarrow wrote in #54 (comment):

That should be good enough, that's basically what we/Nintendo did as well. My only suggestion would be to use a proper error code here since a StationURL longer than 1024 is invalid, but the rejection itself handles the fix just fine

I might add the error code to the refactor but honestly if someone has a station URL of 1024+ that's probably not a real console anyways so the error code would have no effect

@jonbarrow wrote in https://git.spbr.net/spacebar/rust-nex/issues/54#issuecomment-2181: > That should be good enough, that's basically what we/Nintendo did as well. My only suggestion would be to use a proper error code here since a StationURL longer than 1024 is invalid, but the rejection itself handles the fix just fine I might add the error code to the refactor but honestly if someone has a station URL of 1024+ that's probably not a real console anyways so the error code would have no effect
redbinder0526 2026-08-07 18:17:20 +02:00
  • closed this issue
  • added the
    bug
    label
Author

Fair enough. Sounds good, always happy to help

Fair enough. Sounds good, always happy to help
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
spacebar/rust-nex#54
No description provided.