Syllabus Lesson 62 of 156 · Script injection (XSS)
Script injection (XSS)

What an attacker gets

Why it matters "It is only an alert box" is how XSS gets ignored. A script that runs in your page runs as your page, with the visitor's login. Seeing what it can reach, and what the HttpOnly flag does and does not stop, shows why escaping is the real fix.

An analogy: a stranger inside the building

Once someone slips into an office building, a locked drawer stops them reading the keys inside it. But they are still in the building: they can walk up to the front desk and say "I work here, please do this for me". HttpOnly is the locked drawer. Escaping is the door that never let them in.

Session cookie: the cookie a site sets after you log in, so it knows the next request is still you. Anyone holding a copy can act as you until it expires.

HttpOnly: a flag on a cookie that hides it from page scripts: document.cookie does not list it, and scripts cannot overwrite it. The browser still sends it with requests, as usual. You met it from the visitor's side in Lesson 9.

A simulated session theft

Our toy browser can be given the cookies a site has set. If a script on the page mentions document.cookie, the toy pretends it read every cookie scripts can see and sent them to prize.example. send(...) below is pretend code, not a real function; the toy only notes the word document.cookie.

from watched.leg3c_toypages import ATTACK_COMMENT, comment_page, load

print(ATTACK_COMMENT)      # <script>send(document.cookie)</script>
page = load(comment_page(ATTACK_COMMENT), cookies=["session=s-81f2; Path=/; Secure; SameSite=Lax"])
print(page.script_ran, page.stolen, page.attacker_saw_session)
# True {'session': 's-81f2'} True

With that copied value, the attacker's own browser could present itself as the victim. Now add HttpOnly:

page = load(comment_page(ATTACK_COMMENT),
            cookies=["session=s-81f2; Path=/; Secure; SameSite=Lax; HttpOnly"])
print(page.script_ran, page.attacker_saw_session, page.could_act_as_user)
# True False True

The cookie stayed hidden. But look at could_act_as_user: the script still ran in the victim's page, and any request it sends to the same site carries the session cookie automatically. It could change the email address on the account, post as the victim, or read what the page shows. HttpOnly limits the damage; it does not stop the attack.

The fix, in two layers

  • Escape every piece of untrusted text, so no script runs at all. This is the fix.
  • Mark session cookies HttpOnly; Secure; SameSite=Lax, so that if a mistake slips through, the session itself cannot be copied out. This is the safety net. Django's session cookie is HttpOnly by default.

The lab

Write cookie_line(name, value, httponly) and risk_table(comment), which tries the four combinations of "escaped or not" and "HttpOnly or not" and reports what the attacker got in each.

A common misconception

"We use HttpOnly, so XSS is not a big deal for us." Stealing the cookie is only one thing a running script can do. Acting inside the page, as the user, needs no cookie theft at all.

Try it In your own browser, open DevTools on any site you are logged into, go to Application (Storage in Firefox) and then Cookies. The HttpOnly column shows which cookies page scripts cannot read. Session cookies on well-built sites are ticked. Look, but do not copy cookie values anywhere.

In other languages

Setting a session cookie with HttpOnly elsewhere:

// JavaScript (Node.js with Express)
res.cookie("session", id, { httpOnly: true, secure: true, sameSite: "lax" });

// Go (net/http)
http.SetCookie(w, &http.Cookie{Name: "session", Value: id, Path: "/",
    HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode})

# Python (Django settings.py; HttpOnly is already the default)
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True

// C# (ASP.NET Core)
Response.Cookies.Append("session", id, new CookieOptions {
    HttpOnly = true, Secure = true, SameSite = SameSiteMode.Lax });

Recap

  • A script that runs in your page acts with the visitor's login.
  • HttpOnly hides a cookie from scripts, so the session cannot be copied out.
  • HttpOnly does not stop a running script from acting as the user.
  • Escaping is the fix; HttpOnly, Secure and SameSite are the safety net.
Your turn

Write cookie_line(name, value, httponly), returning NAME=VALUE; Path=/; Secure; SameSite=Lax, with ; HttpOnly added at the end only when httponly is True. Then write risk_table(comment), returning a list of 4 dicts in this order: escaped False/HttpOnly False, escaped False/HttpOnly True, escaped True/HttpOnly False, escaped True/HttpOnly True. For each, build the page with comment_page(body) (where body is the comment, run through html.escape when escaped), load it with cookies=[cookie_line("session", "s-81f2", httponly)], and record the keys escaped, httponly, script_ran, attacker_saw_session and could_act_as_user.

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output