Millet Porridge

English version of https://corvo.myseu.cn

0%

An XSS Hole I Left By Accident

No matter what parameters a user passes to a web page, never display them directly. Best to interact with the server once; at the very least escape everything in JS. Never render directly.

TL;DR

The Basic Requirement

When we view a product on a web page, we may only see a name; then we want to click for details, and the new page shows the detailed information.

My Shortcut

When I wrote the program, the homepage had actually already requested the detailed information — it just displayed part of it. If I carried that information over to the detail page, I could save one request to the backend. That is, the content of the current page had to be passed to the next page.

I referred to the approach in passing parameters in html page jumps: encode the page content and pass it to the next page. The JS code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
// index.js
let data={
"name": "a good",
"price": 21.0,
"desc": "a good only use to test",
};

$("#detail").on("click", function() {// when the button is clicked, the page jumps with a get parameter
let data_pretty = JSON.stringify(data); // serialize to a string
let info = btoa(data_pretty); // base64 encode
window.location = `detail.html?info=${info}`;
});

xss1

After clicking the button, the requested link looks roughly like this: http://127.0.0.1:8000/detail.html?info=eyJuYW1lIjoiYSBnb29kIiwicHJpY2UiOjIxLCJkZXNjIjoiYSBnb29kIG9ubHkgdXNlIHRvIHRlc3QifQ==

Then in detail.html, extract the parameter somehow, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
// detail.js
// receive request parameters from other pages
function getParams(key) {
var reg = new RegExp("(^|&)" + key + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) {
return unescape(r[2]);
}
return null;
}

let info = getParams("info"); // extract the get parameter
let data_info = JSON.parse(atob(info));

Then rendering it onto the page is a very simple matter.

xss1

The code above is in the Github repository; feel free to look.

I simplified the example code: the homepage only shows a product name; after clicking the detail button, the jumped-to page can show the detailed information.

If a user shares the link, they need to share that long URL string with others, and others can also get the product information by clicking the link. Everyone’s happy — so why is there a vulnerability?

How to Inject

Encoding the information before passing it is indeed convenient — but what if a hacker modifies the URL? We simply cannot control how shared links spread. Once we know how the page renders, injection is simple.

Suppose the product information we pass is changed to the following.

1
2
3
let xss_data = {
"data" : "Comment#><img src=x onerror=alert(1)//>"
};

xss3

After encoding, replace the info parameter and request the page again: http://127.0.0.1:8000/detail.html?info=eyJkYXRhIjoiQ29tbWVudCM+PGltZyBzcmM9eCBvbmVycm9yPWFsZXJ0KDEpLy8+In0=

xss5

alert(1) starts working, which means you can write any form of JS code to run on the page.

If a malicious person tampers with the URL and injects their own script, they can easily obtain the cookie (they just need to send one request to their own server carrying the cookie).

I’m not very proficient in XSS injection; I referred to the injection methods on this site: xss-bypass.

Summary

We must never, for the sake of convenience, render information present in the URL directly into HTML. Even if you encrypt your real information, since JavaScript also runs on the user’s side, hackers have all kinds of ways to blow up your JS, then add any script they want — and you will watch helplessly as your users click links they shouldn’t click.