
XSS (cross-site scripting) uppstår när text som någon annan har skrivit tolkas som HTML eller JavaScript i webbläsaren. OWASP:s Cross Site Scripting Prevention Cheat Sheet sammanfattar skyddet efter hur datan används: föredra API:er som behandlar data som text, använd kontextberoende utdatakodning när värdet hamnar i en kontext som tolkar syntax, och använd en underhållen HTML-sanerare när användaren faktiskt ska få skriva HTML. Indatavalidering ersätter inte något av de tre.
Mönstret: formateringen som gör texten till kod
Be en kodassistent om ”visa kommentaren och behåll radbrytningarna” och du kan få något i stil med exemplet nedan. Exemplet är konstruerat för artikeln och är inget mätresultat om hur ofta en viss modell skriver så.
// Före: kommentaren tolkas som HTML
function showComment(el, comment) {
el.innerHTML = comment.replace(/\n/g, '<br>');
}
Det fungerar i varje vanligt test, eftersom vanliga kommentarer inte innehåller taggar. Men innerHTML tolkar hela strängen. MDN:s säkerhetsavsnitt om innerHTML påpekar att egenskapen visserligen hindrar inskjutna <script>-element från att köras, men att HTML kan köra JavaScript på andra sätt. MDN:s exempel är en bild med ogiltig källa och en onerror-hanterare. Att granskaren inte ser någon script-tagg i diffen bevisar alltså ingenting.
Samma mönster finns i ramverken. Reacts referens varnar för att dangerouslySetInnerHTML med HTML som inte är betrodd, till exempel baserad på användardata, riskerar en XSS-sårbarhet. Vues dokumentation skriver att v-html bara ska användas på betrott innehåll och aldrig på användarinnehåll. På servern motsvaras det av mallar där escapingen stängts av för ett värde.
Sök efter sänkorna i din kodbas
En sänka är stället där ett värde sätts in i sidan. Tre sökningar med ripgrep ger en första lista. Mönstren saknar citattecken inuti och fungerar därför likadant i PowerShell och bash:
# DOM-API:er som tolkar HTML
rg -n '\.(inner|outer)HTML\s*\+?=|insertAdjacentHTML|document\.write'
# Ramverkens nödutgångar i React och Vue
rg -n 'dangerouslySetInnerHTML|v-html'
# Servermallar där escapingen stängs av: Jinja |safe, Handlebars {{{ }}}
rg -n '\|\s*safe\b|\{\{\{'
I Jinja gör filtret |safe att ett värde inte escapas även när automatisk escaping är påslagen, och standardkonfigurationen har ingen automatisk escaping alls. Kontrollera därför även att miljön är konfigurerad med autoescape. I Handlebars escapas {{uttryck}}, medan trippla klamrar ger rå utdata.
Vi provade mönstren den 3 november 2026 med ripgrep 15.0.0 på en provmapp med 14 rader: nio sänkrader och fem säkra motsvarigheter (textContent, insertAdjacentText, {bio} i JSX, v-text och {{ comment }}). Alla nio sänkrader träffades och ingen av de fem säkra. Det säger ingenting om träffbilden i en riktig kodbas. Mönstren läser en rad i taget och missar alias och dynamiska egenskapsnamn. Använder projektet andra ramverk, lägg till deras nödutgångar; OWASP nämner bland annat Angulars bypassSecurityTrustAs* och Lits unsafeHTML.
Klassa varje träff: säker, escapad eller sanerad
Gå igenom listan och ge varje träff en av tre klasser. En träff som inte passar in i någon av dem ska rättas. Klasserna är vår tillämpning av OWASP:s tre skydd.
- Säker: värdet går till ett API som behandlar det som text. OWASP:s råd är att skriva om kod som använder osäkra sänkor som
innerHTMLså att den användertextContentellervalue, och MDN skriver detsamma för innehåll som ska vara ren text. I Vue sätterv-textoch{{ }}elementets textinnehåll. I React är motsvarigheten att rendera värdet som barn,<p>{bio}</p>, i stället för i__html. Undantag enligt OWASP: textsänkor påscript- ochstyle-element sätter körbar kod. - Escapad: värdet sätts in i en mall som kodar det för sin kontext. OWASP säger att du ska börja med ramverkets inbyggda utdatakodning och använda ett kodningsbibliotek där ramverket inte räcker. Kodningen beror på kontexten: HTML, attribut, JavaScript, CSS och URL tolkas olika, och fel metod kan ge nya svagheter. Attributvärden ska stå inom citattecken. Lägg aldrig värden direkt i ett script-block, en HTML-kommentar, ett style-block eller en händelsehanterare; där räcker kodning inte fullt ut enligt OWASP.
- Sanerad: användaren ska verkligen få skriva HTML, till exempel i en WYSIWYG-redigerare, och då skulle kodning ta bort formateringen. OWASP rekommenderar DOMPurify för sanering, med tre förbehåll: ändra inte innehållet efter saneringen, kontrollera att biblioteket som tar emot strängen inte muterar den och uppdatera saneraren regelbundet.
Rättningen av exemplet ovan blir ett textanrop. Funktionen har samma namn och argument; skillnaden ligger i tilldelningen:
// Efter: kommentaren blir en textnod
function showComment(el, comment) {
el.textContent = comment;
}
Radbrytningarna sköts sedan av CSS med white-space: pre-line på kommentarens element. Ska användaren få skriva Markdown med länkar och fetstil hör funktionen till klassen sanerad. I React skapar du då objektet nära där HTML:en genereras, vilket Reacts referens rekommenderar, och sanerar innan det lämnas över:
import DOMPurify from 'dompurify';
function toMarkup(html) {
return { __html: DOMPurify.sanitize(html) };
}
// <div dangerouslySetInnerHTML={toMarkup(renderedMarkdown)} />
Reacts egen formulering är att dangerouslySetInnerHTML bara ska användas med betrodd och sanerad data. Notera i granskningen vilket bibliotek och vilken version som sanerar, så att uppdateringen inte glöms.
Bevisa rättningen med en ofarlig nyttolast
Ett bra prov visar två saker: att nyttolasten inte körs och att den syns som exakt den text som skickades in. Nyttolasten nedan gör bara en sak om den körs: den sätter ett data-attribut på sitt eget föräldraelement. Den skickar inget och läser inget. Kör provet lokalt i en egen fil eller ditt eget projekt, aldrig mot någon annans system.
Spara som xss-text-test.html och öppna filen i webbläsaren. Resultatet skrivs i sidan som JSON:
<!doctype html>
<meta charset="utf-8">
<title>Lokalt XSS-prov</title>
<div id="before"></div>
<div id="after"></div>
<pre id="result"></pre>
<script>
// Ofarlig nyttolast: om den körs sätter den bara en markering på föräldern.
const PAYLOAD = '<b>fet?</b><img src="x" onerror="this.parentNode.dataset.ran = \'ja\'">';
function renderBefore(el, text) { el.innerHTML = text; }
function renderAfter(el, text) { el.textContent = text; }
const before = document.getElementById('before');
const after = document.getElementById('after');
renderBefore(before, PAYLOAD);
renderAfter(after, PAYLOAD);
setTimeout(() => {
const report = {
before: { ran: before.dataset.ran === 'ja', elements: before.children.length },
after: { ran: after.dataset.ran === 'ja', elements: after.children.length,
textMatches: after.textContent === PAYLOAD },
};
report.pass = !report.after.ran && report.after.elements === 0 && report.after.textMatches;
document.getElementById('result').textContent = JSON.stringify(report);
}, 500);
</script>
Faktiskt utfört prov: den 3 november 2026 öppnades filen lokalt i Microsoft Edge 156.0.4314.58 i huvudlöst läge på Windows. Resultatet blev {"before":{"ran":true,"elements":2},"after":{"ran":false,"elements":0,"textMatches":true},"pass":true}. Med innerHTML kördes händelsehanteraren och två element skapades. Med textContent kördes ingenting, inga element skapades och den synliga texten var identisk med nyttolasten, vinkelparenteser inräknade.
Provet gäller en webbläsare och den visade filen, utan ramverk, sanerare eller CSP. Flytta samma tre kontroller till ett komponent- eller end-to-end-test i ditt eget projekt: rendera nyttolasten genom den riktiga komponenten, kontrollera att markeringen saknas, att inga oväntade element finns och att texten stämmer tecken för tecken. Den sista kontrollen fångar också dubbelkodning, där användaren plötsligt ser < i stället för <. För sanerade träffar byts förväntan: tillåten formatering ska finnas kvar, men markeringen ska fortfarande saknas.
CSP är ett extra lager, inte rättningen
En Content Security Policy kan begränsa vad en sida får ladda och köra. OWASP beskriver den som ett extra försvarslager och avråder från att göra den till det primära skyddet, eftersom det är lätt att göra fel i implementationen. Samma sak gäller Trusted Types, som MDN och OWASP beskriver via CSP-direktivet require-trusted-types-for: sänkorna kräver då typade värden i stället för vanliga strängar, men OWASP påpekar att Trusted Types inte sanerar något själv. Policyn måste fortfarande validera eller sanera.
Artikeln är en del av en serie om sårbarheter i AI-genererad kod. Om samma kodbas bygger databasfrågor av indata, läs SQL-injektion i AI-genererad kod; där är principen densamma, att indata ska förbli data. Resten av granskningen samlas i säkerhetsguiden.
FAQ
Räcker det att innerHTML inte kör script-taggar?
Nej. Enligt MDN hindrar innerHTML inskjutna script-element från att köras, men HTML kan köra JavaScript på andra sätt, till exempel genom en bild med onerror. I vårt lokala prov kördes just en sådan händelsehanterare.
Är dangerouslySetInnerHTML alltid fel i React?
Nej, men React skriver att den bara ska användas med betrodd och sanerad data. Visa vanlig användartext som text, och sanera med ett etablerat bibliotek som DOMPurify när HTML verkligen behövs.
Ersätter en Content Security Policy escaping och sanering?
Nej. OWASP beskriver CSP som ett extra försvarslager och avråder från att göra det till det primära skyddet, eftersom det är lätt att göra fel i implementationen.
Hur bevisar jag att rättningen fungerar?
Rendera en ofarlig nyttolast lokalt i den rättade koden och kontrollera tre saker: att ingen händelsehanterare kördes, att inga nya element skapades och att den synliga texten är identisk med nyttolasten.
Källor och metod
Källorna kontrollerades 3 november 2026: OWASP Cross Site Scripting Prevention Cheat Sheet (läst i källtexten på GitHub) för de tre skydden, säkra sänkor, kontexter, DOMPurify, ramverkens nödutgångar, CSP och Trusted Types; Reacts referens för gemensamma komponenter för dangerouslySetInnerHTML; MDN om innerHTML för script-element, onerror, Trusted Types och textContent; Vues direktivreferens för v-html och v-text; Jinjas malldokumentation för |safe och autoescape; Handlebars guide om uttryck för trippla klamrar. Kodexemplen, sökmönstren och provfilen är AI-kreatörs egna; DOMPurify-exemplet följer OWASP:s exempel och har inte provkörts här. Artikeln gör inget påstående om hur ofta AI-verktyg skriver kod med XSS-hål.