Utdatakodning och Content Security Policy
Observera
Genomför PortSwigger-akademins XSS-moduler eller DVWA:s XSS-övningar (Reflected och Stored), på nivå Low och därefter High. OWASP Juice Shop har också bra, guidade utmaningar här.
Notera särskilt skillnaden mellan reflected (data studsar tillbaka i svaret på en förfrågan) och stored (data sparas och visas sedan för alla besökare). Den senare är allvarligare eftersom den drabbar andra användare utan att de gör något ovanligt.
Analysera grundorsaken
Grundorsaken är spegelbilden av Lab 3: användardata placeras i ett HTML-dokument utan att kodas om för det sammanhang den hamnar i. Webbläsaren kan inte skilja på innehåll som applikationen menade som text och innehåll som råkar vara markup.
Studera källkoden i DVWA mellan nivåerna och identifiera var utdata skrivs ut. Lägg märke till att sammanhanget spelar roll — samma data behöver olika behandling beroende på om den hamnar i HTML-text, i ett attribut, i en URL eller i ett JavaScript-block.
Bygg försvaret
1. Kontextkorrekt utdatakodning. Använd ramverkets inbyggda mekanismer, som gör detta rätt som standard:
| Ramverk | Säkert som standard | Farlig konstruktion att undvika |
|---|---|---|
| React | {variabel} |
dangerouslySetInnerHTML |
| Vue | {{ variabel }} |
v-html |
| Django | {{ variabel }} |
\|safe, mark_safe() |
| Jinja2 | {{ variabel }} (med autoescape) |
\|safe |
| PHP | htmlspecialchars($v, ENT_QUOTES, 'UTF-8') |
direkt echo $v |
Regeln: om du letar efter ett sätt att stänga av ramverkets escaping har du nästan alltid hamnat fel.
2. Content Security Policy. CSP är ett andra lager som begränsar vad webbläsaren överhuvudtaget får köra, även om något skulle slinka igenom:
add_header Content-Security-Policy
"default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
Vad koden gör — förklaring rad för rad
En nginx-rad som skickar med en Content Security Policy. Den talar om för webbläsaren vad den överhuvudtaget får köra på sidan — ett andra skyddslager om något skulle slinka igenom.
- default-src 'self'
- Som utgångspunkt: ladda bara saker från vår egen domän.
- script-src 'self'
- Kör bara JavaScript från vår egen domän. Skript inskrivna direkt i sidan blockeras, vilket är just det en angripare skulle försöka smuggla in.
- object-src 'none'
- Tillåt inga inbäddade objekt som Flash eller liknande. none betyder ingenting alls.
- frame-ancestors 'none'
- Ingen annan webbplats får bädda in din sida i en ram. Skyddar mot att din sida läggs osynlig ovanpå en annan.
script-src 'self' innebär att endast skript från den egna domänen körs — inline-skript blockeras. Det kräver att applikationen är byggd utan inline-JavaScript, vilket är gott hantverk i sig.
3. HttpOnly på sessionskakan (se Lab 1) gör att sessionskakan inte kan läsas av JavaScript, vilket begränsar konsekvensen av ett problem.
4. Rensa HTML när användare ska få använda formatering. Om användare legitimt ska kunna skriva fetstil och länkar räcker inte escaping — då behövs ett bibliotek som rensar bort allt utom en vitlista av tillåtna taggar. Använd ett etablerat bibliotek (DOMPurify för JavaScript, Bleach för Python, HTML Purifier för PHP). Skriv aldrig en egen HTML-rensare; det är ett problem som är betydligt svårare än det ser ut.
Verifiera
Kontrollera att CSP-headern faktiskt levereras:
curl -I https://exempel.se | grep -i content-security-policy
Vad koden gör — förklaring rad för rad
Ett kontrollkommando som bekräftar att policyn faktiskt skickas med. Två verktyg kedjas ihop.
- curl -I https://exempel.se
- Hämtar svarshuvudena från sajten.
- |
- Det lodräta strecket kallas pipe. Det skickar resultatet från kommandot till vänster vidare in i kommandot till höger.
- grep -i content-security-policy
- grep letar efter en viss text och visar bara de rader som matchar. -i betyder att stora och små bokstäver behandlas lika.
Öppna webbläsarens utvecklarkonsol och leta efter CSP-överträdelser — de visas som tydliga felmeddelanden och är ett utmärkt sätt att hitta kvarvarande inline-skript i din egen applikation.
Dokumentera
Dokumentera både typen (reflected/stored) och sammanhanget där problemet uppstod, eftersom åtgärden skiljer sig åt. Bifoga en rekommendation i två lager: kodningen som primärt försvar och CSP som sekundärt.
Reflektionsfrågor: Varför är försvar i flera lager ("defense in depth") särskilt relevant här? Vad händer med CSP-skyddet om applikationen använder 'unsafe-inline'?
Kunskapskontroll
Välj ett svar per fråga. Du får återkoppling direkt, och avsnittet markeras som klart när alla frågor är besvarade.
1Varför är lagrad (stored) XSS allvarligare än reflekterad?
2Vad innebär `script-src 'self'` i en Content Security Policy?
3Användare ska legitimt kunna skriva fetstil och länkar. Vad krävs utöver escaping?
Output encoding and Content Security Policy
Observe
Work through PortSwigger Academy's XSS modules or DVWA's XSS exercises (Reflected and Stored), at Low and then High. OWASP Juice Shop also has good guided challenges here.
Note especially the difference between reflected (data bounces back in the response to a request) and stored (data is saved and later shown to every visitor). The latter is more severe because it affects other users without them doing anything unusual.
Analyze the root cause
The root cause is the mirror image of Lab 3: user data is placed into an HTML document without being encoded for the context it lands in. The browser cannot distinguish content the application meant as text from content that happens to be markup.
Study the DVWA source across levels and identify where output is written. Note that context matters — the same data needs different treatment depending on whether it lands in HTML text, an attribute, a URL, or a JavaScript block.
Build the defense
1. Context-correct output encoding. Use the framework's built-in mechanisms, which get this right by default:
| Framework | Safe by default | Dangerous construct to avoid |
|---|---|---|
| React | {variable} |
dangerouslySetInnerHTML |
| Vue | {{ variable }} |
v-html |
| Django | {{ variable }} |
\|safe, mark_safe() |
| Jinja2 | {{ variable }} (with autoescape) |
\|safe |
| PHP | htmlspecialchars($v, ENT_QUOTES, 'UTF-8') |
bare echo $v |
The rule: if you are hunting for a way to switch off your framework's escaping, you have almost always taken a wrong turn.
2. Content Security Policy. CSP is a second layer that restricts what the browser is allowed to execute at all, even if something does slip through:
add_header Content-Security-Policy
"default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'" always;
What this code does — line by line
An nginx line attaching a Content Security Policy. It tells the browser what it is allowed to run on the page at all — a second layer of protection should something slip through.
- default-src 'self'
- As a baseline: only load things from our own domain.
- script-src 'self'
- Only run JavaScript from our own domain. Scripts written directly into the page are blocked, which is exactly what an attacker would try to smuggle in.
- object-src 'none'
- Permit no embedded objects such as Flash or similar. none means nothing at all.
- frame-ancestors 'none'
- No other website may embed your page in a frame. Protects against your page being laid invisibly over another.
script-src 'self' means only scripts from your own origin run — inline scripts are blocked. This requires the application to be built without inline JavaScript, which is good craftsmanship in its own right.
3. HttpOnly on the session cookie (see Lab 1) prevents the session cookie from being read by JavaScript, limiting the consequences of a problem.
4. Sanitize HTML when users are meant to use formatting. If users legitimately need bold text and links, escaping is not enough — you need a library that strips everything outside an allowlist of permitted tags. Use an established library (DOMPurify for JavaScript, Bleach for Python, HTML Purifier for PHP). Never write your own HTML sanitizer; it is a considerably harder problem than it appears.
Verify
Check the CSP header is actually delivered:
curl -I https://example.com | grep -i content-security-policy
What this code does — line by line
A check command confirming the policy is actually being sent. Two tools chained together.
- curl -I https://example.com
- Fetches the response headers from the site.
- |
- The vertical bar is called a pipe. It feeds the result of the command on the left into the command on the right.
- grep -i content-security-policy
- grep looks for a given piece of text and shows only the matching lines. -i means upper and lower case are treated alike.
Open the browser developer console and look for CSP violations — they appear as clear error messages and are an excellent way to find remaining inline scripts in your own application.
Document
Document both the type (reflected/stored) and the context where the problem occurred, since the remediation differs. Include a two-layer recommendation: encoding as the primary defense, CSP as the secondary.
Reflection: Why is defense in depth particularly relevant here? What happens to CSP protection if the application uses 'unsafe-inline'?
Knowledge check
Pick one answer per question. You get feedback immediately, and the unit is marked complete once every question is answered.
1Why is stored XSS more severe than reflected XSS?
2What does `script-src 'self'` mean in a Content Security Policy?
3Users legitimately need to write bold text and links. What is required beyond escaping?