[{"data":1,"prerenderedAt":3266},["ShallowReactive",2],{"page-\u002Fjs\u002F29-security":3},{"id":4,"title":5,"body":6,"description":3259,"extension":3260,"meta":3261,"navigation":70,"path":3262,"seo":3263,"stem":3264,"__hash__":3265},"content\u002Fjs\u002F29-security.md","JavaScript 29 — Security: XSS, CSRF, CSP & Prototype Pollution",{"type":7,"value":8,"toc":3247},"minimark",[9,14,19,545,549,981,985,1252,1256,1494,1498,1946,1950,2328,2332,2412,2416,2514,2518,2679,2683,2687,2826,3243],[10,11,13],"h1",{"id":12},"_29-security-xss-csrf-csp-prototype-pollution","29 — Security: XSS, CSRF, CSP & Prototype Pollution",[15,16,18],"h2",{"id":17},"xss-cross-site-scripting-prevention","XSS: Cross-Site Scripting Prevention",[20,21,23],"code-wrapper",{"language":22},"javascript",[24,25,29],"pre",{"className":26,"code":27,"language":22,"meta":28,"style":28},"language-javascript shiki shiki-themes github-light github-dark","\u002F\u002F ── XSS: attacker injects malicious HTML\u002FJS into a page ──\n\u002F\u002F Three types:\n\u002F\u002F 1. Stored XSS: payload saved in DB, rendered for all users (most severe)\n\u002F\u002F 2. Reflected XSS: payload in URL, reflected back in the page\n\u002F\u002F 3. DOM-based XSS: payload injected via client-side DOM manipulation\n\n\u002F\u002F ❌ NAIVE — innerHTML renders raw HTML (XSS vulnerability)\nfunction renderUserInput(input) {\n    document.querySelector(\"#output\").innerHTML = input;  \u002F\u002F XSS!\n}\nrenderUserInput('\u003Cimg src=x onerror=\"alert(document.cookie)\">');  \u002F\u002F script executes\n\n\u002F\u002F ✅ SAFE — textContent treats input as text (no HTML parsing)\nfunction renderSafe(input) {\n    document.querySelector(\"#output\").textContent = input;  \u002F\u002F escaped as text\n}\nrenderSafe('\u003Cimg src=x onerror=\"alert(1)\">');  \u002F\u002F displays as literal text\n\n\u002F\u002F ✅ SAFE — sanitize HTML with DOMPurify (allow safe tags, strip scripts)\nimport DOMPurify from \"dompurify\";\nfunction renderSanitized(html) {\n    const clean = DOMPurify.sanitize(html, {\n        ALLOWED_TAGS: [\"b\", \"i\", \"em\", \"strong\", \"a\", \"p\", \"br\"],\n        ALLOWED_ATTR: [\"href\", \"title\"],\n        FORBID_ATTR: [\"style\", \"onerror\", \"onclick\"],  \u002F\u002F explicit deny list\n    });\n    document.querySelector(\"#output\").innerHTML = clean;\n}\nrenderSanitized('\u003Cb>safe\u003C\u002Fb>\u003Cscript>alert(\"xss\")\u003C\u002Fscript>');  \u002F\u002F \u003Cb>safe\u003C\u002Fb> (script stripped)\n\n\u002F\u002F ── Dangerous sinks (never pass user input to these) ──\nelement.innerHTML = userInput;       \u002F\u002F ❌ HTML injection\ndocument.write(userInput);           \u002F\u002F ❌ overwrites the page\nelement.setAttribute(\"onclick\", userInput);  \u002F\u002F ❌ event handler injection\nelement.outerHTML = userInput;       \u002F\u002F ❌ replaces element\neval(userInput);                     \u002F\u002F ❌ code execution\nsetTimeout(userInput, 0);            \u002F\u002F ❌ string arg = eval\nsetInterval(userInput, 1000);        \u002F\u002F ❌ string arg = eval\nnew Function(userInput);             \u002F\u002F ❌ creates a function from a string\n","",[30,31,32,41,47,53,59,65,72,78,100,127,133,150,155,161,175,196,201,217,222,228,246,261,283,326,342,367,373,391,396,412,417,423,437,452,471,484,496,514,530],"code",{"__ignoreMap":28},[33,34,37],"span",{"class":35,"line":36},"line",1,[33,38,40],{"class":39},"sdCPZ","\u002F\u002F ── XSS: attacker injects malicious HTML\u002FJS into a page ──\n",[33,42,44],{"class":35,"line":43},2,[33,45,46],{"class":39},"\u002F\u002F Three types:\n",[33,48,50],{"class":35,"line":49},3,[33,51,52],{"class":39},"\u002F\u002F 1. Stored XSS: payload saved in DB, rendered for all users (most severe)\n",[33,54,56],{"class":35,"line":55},4,[33,57,58],{"class":39},"\u002F\u002F 2. Reflected XSS: payload in URL, reflected back in the page\n",[33,60,62],{"class":35,"line":61},5,[33,63,64],{"class":39},"\u002F\u002F 3. DOM-based XSS: payload injected via client-side DOM manipulation\n",[33,66,68],{"class":35,"line":67},6,[33,69,71],{"emptyLinePlaceholder":70},true,"\n",[33,73,75],{"class":35,"line":74},7,[33,76,77],{"class":39},"\u002F\u002F ❌ NAIVE — innerHTML renders raw HTML (XSS vulnerability)\n",[33,79,81,85,89,93,97],{"class":35,"line":80},8,[33,82,84],{"class":83},"svdQ7","function",[33,86,88],{"class":87},"sIsaT"," renderUserInput",[33,90,92],{"class":91},"ssxIu","(",[33,94,96],{"class":95},"sCrzJ","input",[33,98,99],{"class":91},") {\n",[33,101,103,106,109,111,115,118,121,124],{"class":35,"line":102},9,[33,104,105],{"class":91},"    document.",[33,107,108],{"class":87},"querySelector",[33,110,92],{"class":91},[33,112,114],{"class":113},"sJ6F3","\"#output\"",[33,116,117],{"class":91},").innerHTML ",[33,119,120],{"class":83},"=",[33,122,123],{"class":91}," input;  ",[33,125,126],{"class":39},"\u002F\u002F XSS!\n",[33,128,130],{"class":35,"line":129},10,[33,131,132],{"class":91},"}\n",[33,134,136,139,141,144,147],{"class":35,"line":135},11,[33,137,138],{"class":87},"renderUserInput",[33,140,92],{"class":91},[33,142,143],{"class":113},"'\u003Cimg src=x onerror=\"alert(document.cookie)\">'",[33,145,146],{"class":91},");  ",[33,148,149],{"class":39},"\u002F\u002F script executes\n",[33,151,153],{"class":35,"line":152},12,[33,154,71],{"emptyLinePlaceholder":70},[33,156,158],{"class":35,"line":157},13,[33,159,160],{"class":39},"\u002F\u002F ✅ SAFE — textContent treats input as text (no HTML parsing)\n",[33,162,164,166,169,171,173],{"class":35,"line":163},14,[33,165,84],{"class":83},[33,167,168],{"class":87}," renderSafe",[33,170,92],{"class":91},[33,172,96],{"class":95},[33,174,99],{"class":91},[33,176,178,180,182,184,186,189,191,193],{"class":35,"line":177},15,[33,179,105],{"class":91},[33,181,108],{"class":87},[33,183,92],{"class":91},[33,185,114],{"class":113},[33,187,188],{"class":91},").textContent ",[33,190,120],{"class":83},[33,192,123],{"class":91},[33,194,195],{"class":39},"\u002F\u002F escaped as text\n",[33,197,199],{"class":35,"line":198},16,[33,200,132],{"class":91},[33,202,204,207,209,212,214],{"class":35,"line":203},17,[33,205,206],{"class":87},"renderSafe",[33,208,92],{"class":91},[33,210,211],{"class":113},"'\u003Cimg src=x onerror=\"alert(1)\">'",[33,213,146],{"class":91},[33,215,216],{"class":39},"\u002F\u002F displays as literal text\n",[33,218,220],{"class":35,"line":219},18,[33,221,71],{"emptyLinePlaceholder":70},[33,223,225],{"class":35,"line":224},19,[33,226,227],{"class":39},"\u002F\u002F ✅ SAFE — sanitize HTML with DOMPurify (allow safe tags, strip scripts)\n",[33,229,231,234,237,240,243],{"class":35,"line":230},20,[33,232,233],{"class":83},"import",[33,235,236],{"class":91}," DOMPurify ",[33,238,239],{"class":83},"from",[33,241,242],{"class":113}," \"dompurify\"",[33,244,245],{"class":91},";\n",[33,247,249,251,254,256,259],{"class":35,"line":248},21,[33,250,84],{"class":83},[33,252,253],{"class":87}," renderSanitized",[33,255,92],{"class":91},[33,257,258],{"class":95},"html",[33,260,99],{"class":91},[33,262,264,267,271,274,277,280],{"class":35,"line":263},22,[33,265,266],{"class":83},"    const",[33,268,270],{"class":269},"snvgF"," clean",[33,272,273],{"class":83}," =",[33,275,276],{"class":91}," DOMPurify.",[33,278,279],{"class":87},"sanitize",[33,281,282],{"class":91},"(html, {\n",[33,284,286,289,292,295,298,300,303,305,308,310,313,315,318,320,323],{"class":35,"line":285},23,[33,287,288],{"class":91},"        ALLOWED_TAGS: [",[33,290,291],{"class":113},"\"b\"",[33,293,294],{"class":91},", ",[33,296,297],{"class":113},"\"i\"",[33,299,294],{"class":91},[33,301,302],{"class":113},"\"em\"",[33,304,294],{"class":91},[33,306,307],{"class":113},"\"strong\"",[33,309,294],{"class":91},[33,311,312],{"class":113},"\"a\"",[33,314,294],{"class":91},[33,316,317],{"class":113},"\"p\"",[33,319,294],{"class":91},[33,321,322],{"class":113},"\"br\"",[33,324,325],{"class":91},"],\n",[33,327,329,332,335,337,340],{"class":35,"line":328},24,[33,330,331],{"class":91},"        ALLOWED_ATTR: [",[33,333,334],{"class":113},"\"href\"",[33,336,294],{"class":91},[33,338,339],{"class":113},"\"title\"",[33,341,325],{"class":91},[33,343,345,348,351,353,356,358,361,364],{"class":35,"line":344},25,[33,346,347],{"class":91},"        FORBID_ATTR: [",[33,349,350],{"class":113},"\"style\"",[33,352,294],{"class":91},[33,354,355],{"class":113},"\"onerror\"",[33,357,294],{"class":91},[33,359,360],{"class":113},"\"onclick\"",[33,362,363],{"class":91},"],  ",[33,365,366],{"class":39},"\u002F\u002F explicit deny list\n",[33,368,370],{"class":35,"line":369},26,[33,371,372],{"class":91},"    });\n",[33,374,376,378,380,382,384,386,388],{"class":35,"line":375},27,[33,377,105],{"class":91},[33,379,108],{"class":87},[33,381,92],{"class":91},[33,383,114],{"class":113},[33,385,117],{"class":91},[33,387,120],{"class":83},[33,389,390],{"class":91}," clean;\n",[33,392,394],{"class":35,"line":393},28,[33,395,132],{"class":91},[33,397,399,402,404,407,409],{"class":35,"line":398},29,[33,400,401],{"class":87},"renderSanitized",[33,403,92],{"class":91},[33,405,406],{"class":113},"'\u003Cb>safe\u003C\u002Fb>\u003Cscript>alert(\"xss\")\u003C\u002Fscript>'",[33,408,146],{"class":91},[33,410,411],{"class":39},"\u002F\u002F \u003Cb>safe\u003C\u002Fb> (script stripped)\n",[33,413,415],{"class":35,"line":414},30,[33,416,71],{"emptyLinePlaceholder":70},[33,418,420],{"class":35,"line":419},31,[33,421,422],{"class":39},"\u002F\u002F ── Dangerous sinks (never pass user input to these) ──\n",[33,424,426,429,431,434],{"class":35,"line":425},32,[33,427,428],{"class":91},"element.innerHTML ",[33,430,120],{"class":83},[33,432,433],{"class":91}," userInput;       ",[33,435,436],{"class":39},"\u002F\u002F ❌ HTML injection\n",[33,438,440,443,446,449],{"class":35,"line":439},33,[33,441,442],{"class":91},"document.",[33,444,445],{"class":87},"write",[33,447,448],{"class":91},"(userInput);           ",[33,450,451],{"class":39},"\u002F\u002F ❌ overwrites the page\n",[33,453,455,458,461,463,465,468],{"class":35,"line":454},34,[33,456,457],{"class":91},"element.",[33,459,460],{"class":87},"setAttribute",[33,462,92],{"class":91},[33,464,360],{"class":113},[33,466,467],{"class":91},", userInput);  ",[33,469,470],{"class":39},"\u002F\u002F ❌ event handler injection\n",[33,472,474,477,479,481],{"class":35,"line":473},35,[33,475,476],{"class":91},"element.outerHTML ",[33,478,120],{"class":83},[33,480,433],{"class":91},[33,482,483],{"class":39},"\u002F\u002F ❌ replaces element\n",[33,485,487,490,493],{"class":35,"line":486},36,[33,488,489],{"class":87},"eval",[33,491,492],{"class":91},"(userInput);                     ",[33,494,495],{"class":39},"\u002F\u002F ❌ code execution\n",[33,497,499,502,505,508,511],{"class":35,"line":498},37,[33,500,501],{"class":87},"setTimeout",[33,503,504],{"class":91},"(userInput, ",[33,506,507],{"class":269},"0",[33,509,510],{"class":91},");            ",[33,512,513],{"class":39},"\u002F\u002F ❌ string arg = eval\n",[33,515,517,520,522,525,528],{"class":35,"line":516},38,[33,518,519],{"class":87},"setInterval",[33,521,504],{"class":91},[33,523,524],{"class":269},"1000",[33,526,527],{"class":91},");        ",[33,529,513],{"class":39},[33,531,533,536,539,542],{"class":35,"line":532},39,[33,534,535],{"class":83},"new",[33,537,538],{"class":87}," Function",[33,540,541],{"class":91},"(userInput);             ",[33,543,544],{"class":39},"\u002F\u002F ❌ creates a function from a string\n",[15,546,548],{"id":547},"xss-in-template-literals-and-url-contexts","XSS in Template Literals and URL Contexts",[20,550,551],{"language":22},[24,552,554],{"className":26,"code":553,"language":22,"meta":28,"style":28},"\u002F\u002F ── Template literals with user input → XSS if inserted into HTML ──\nconst userName = '\u003Cimg src=x onerror=\"alert(1)\">';\ndocument.querySelector(\"#greeting\").innerHTML = `Hello, ${userName}!`;  \u002F\u002F ❌ XSS\n\n\u002F\u002F ✅ Escape HTML entities before inserting\nfunction escapeHtml(str) {\n    const div = document.createElement(\"div\");\n    div.textContent = str;  \u002F\u002F browser escapes: \u003C → &lt;, > → &gt;, etc.\n    return div.innerHTML;\n}\ndocument.querySelector(\"#greeting\").innerHTML = `Hello, ${escapeHtml(userName)}!`;\n\n\u002F\u002F ── URL context: javascript: scheme is dangerous ──\nconst userInput = \"javascript:alert(document.cookie)\";\n\u002F\u002F ❌ NAIVE — allows javascript: URLs\ndocument.querySelector(\"#link\").href = userInput;  \u002F\u002F clicking the link executes JS\n\n\u002F\u002F ✅ SAFE — validate the URL scheme\nfunction safeUrl(url) {\n    try {\n        const parsed = new URL(url, window.location.origin);\n        if (parsed.protocol === \"http:\" || parsed.protocol === \"https:\") {\n            return parsed.href;  \u002F\u002F only http\u002Fhttps\n        }\n    } catch { \u002F* invalid URL *\u002F }\n    return \"#\";  \u002F\u002F fallback for dangerous or invalid URLs\n}\ndocument.querySelector(\"#link\").href = safeUrl(userInput);  \u002F\u002F \"#\" (blocked)\n\n\u002F\u002F ── Attribute injection (event handlers in attributes) ──\nconst userClass = 'foo\" onmouseover=\"alert(1)';\n\u002F\u002F ❌ NAIVE — quote breakout\nelement.setAttribute(\"class\", `custom ${userClass}`);  \u002F\u002F class=\"custom foo\" onmouseover=\"alert(1)\"\n\u002F\u002F ✅ SAFE — use setAttribute (auto-escapes quotes in the value)\nelement.setAttribute(\"class\", userClass);  \u002F\u002F quotes escaped, no breakout\n",[30,555,556,561,576,606,610,615,629,652,665,673,677,707,711,716,730,735,757,761,766,780,788,807,834,845,850,867,879,883,905,909,914,928,933,960,965],{"__ignoreMap":28},[33,557,558],{"class":35,"line":36},[33,559,560],{"class":39},"\u002F\u002F ── Template literals with user input → XSS if inserted into HTML ──\n",[33,562,563,566,569,571,574],{"class":35,"line":43},[33,564,565],{"class":83},"const",[33,567,568],{"class":269}," userName",[33,570,273],{"class":83},[33,572,573],{"class":113}," '\u003Cimg src=x onerror=\"alert(1)\">'",[33,575,245],{"class":91},[33,577,578,580,582,584,587,589,591,594,597,600,603],{"class":35,"line":49},[33,579,442],{"class":91},[33,581,108],{"class":87},[33,583,92],{"class":91},[33,585,586],{"class":113},"\"#greeting\"",[33,588,117],{"class":91},[33,590,120],{"class":83},[33,592,593],{"class":113}," `Hello, ${",[33,595,596],{"class":91},"userName",[33,598,599],{"class":113},"}!`",[33,601,602],{"class":91},";  ",[33,604,605],{"class":39},"\u002F\u002F ❌ XSS\n",[33,607,608],{"class":35,"line":55},[33,609,71],{"emptyLinePlaceholder":70},[33,611,612],{"class":35,"line":61},[33,613,614],{"class":39},"\u002F\u002F ✅ Escape HTML entities before inserting\n",[33,616,617,619,622,624,627],{"class":35,"line":67},[33,618,84],{"class":83},[33,620,621],{"class":87}," escapeHtml",[33,623,92],{"class":91},[33,625,626],{"class":95},"str",[33,628,99],{"class":91},[33,630,631,633,636,638,641,644,646,649],{"class":35,"line":74},[33,632,266],{"class":83},[33,634,635],{"class":269}," div",[33,637,273],{"class":83},[33,639,640],{"class":91}," document.",[33,642,643],{"class":87},"createElement",[33,645,92],{"class":91},[33,647,648],{"class":113},"\"div\"",[33,650,651],{"class":91},");\n",[33,653,654,657,659,662],{"class":35,"line":80},[33,655,656],{"class":91},"    div.textContent ",[33,658,120],{"class":83},[33,660,661],{"class":91}," str;  ",[33,663,664],{"class":39},"\u002F\u002F browser escapes: \u003C → &lt;, > → &gt;, etc.\n",[33,666,667,670],{"class":35,"line":102},[33,668,669],{"class":83},"    return",[33,671,672],{"class":91}," div.innerHTML;\n",[33,674,675],{"class":35,"line":129},[33,676,132],{"class":91},[33,678,679,681,683,685,687,689,691,693,696,698,700,703,705],{"class":35,"line":135},[33,680,442],{"class":91},[33,682,108],{"class":87},[33,684,92],{"class":91},[33,686,586],{"class":113},[33,688,117],{"class":91},[33,690,120],{"class":83},[33,692,593],{"class":113},[33,694,695],{"class":87},"escapeHtml",[33,697,92],{"class":113},[33,699,596],{"class":91},[33,701,702],{"class":113},")",[33,704,599],{"class":113},[33,706,245],{"class":91},[33,708,709],{"class":35,"line":152},[33,710,71],{"emptyLinePlaceholder":70},[33,712,713],{"class":35,"line":157},[33,714,715],{"class":39},"\u002F\u002F ── URL context: javascript: scheme is dangerous ──\n",[33,717,718,720,723,725,728],{"class":35,"line":163},[33,719,565],{"class":83},[33,721,722],{"class":269}," userInput",[33,724,273],{"class":83},[33,726,727],{"class":113}," \"javascript:alert(document.cookie)\"",[33,729,245],{"class":91},[33,731,732],{"class":35,"line":177},[33,733,734],{"class":39},"\u002F\u002F ❌ NAIVE — allows javascript: URLs\n",[33,736,737,739,741,743,746,749,751,754],{"class":35,"line":198},[33,738,442],{"class":91},[33,740,108],{"class":87},[33,742,92],{"class":91},[33,744,745],{"class":113},"\"#link\"",[33,747,748],{"class":91},").href ",[33,750,120],{"class":83},[33,752,753],{"class":91}," userInput;  ",[33,755,756],{"class":39},"\u002F\u002F clicking the link executes JS\n",[33,758,759],{"class":35,"line":203},[33,760,71],{"emptyLinePlaceholder":70},[33,762,763],{"class":35,"line":219},[33,764,765],{"class":39},"\u002F\u002F ✅ SAFE — validate the URL scheme\n",[33,767,768,770,773,775,778],{"class":35,"line":224},[33,769,84],{"class":83},[33,771,772],{"class":87}," safeUrl",[33,774,92],{"class":91},[33,776,777],{"class":95},"url",[33,779,99],{"class":91},[33,781,782,785],{"class":35,"line":230},[33,783,784],{"class":83},"    try",[33,786,787],{"class":91}," {\n",[33,789,790,793,796,798,801,804],{"class":35,"line":248},[33,791,792],{"class":83},"        const",[33,794,795],{"class":269}," parsed",[33,797,273],{"class":83},[33,799,800],{"class":83}," new",[33,802,803],{"class":87}," URL",[33,805,806],{"class":91},"(url, window.location.origin);\n",[33,808,809,812,815,818,821,824,827,829,832],{"class":35,"line":263},[33,810,811],{"class":83},"        if",[33,813,814],{"class":91}," (parsed.protocol ",[33,816,817],{"class":83},"===",[33,819,820],{"class":113}," \"http:\"",[33,822,823],{"class":83}," ||",[33,825,826],{"class":91}," parsed.protocol ",[33,828,817],{"class":83},[33,830,831],{"class":113}," \"https:\"",[33,833,99],{"class":91},[33,835,836,839,842],{"class":35,"line":285},[33,837,838],{"class":83},"            return",[33,840,841],{"class":91}," parsed.href;  ",[33,843,844],{"class":39},"\u002F\u002F only http\u002Fhttps\n",[33,846,847],{"class":35,"line":328},[33,848,849],{"class":91},"        }\n",[33,851,852,855,858,861,864],{"class":35,"line":344},[33,853,854],{"class":91},"    } ",[33,856,857],{"class":83},"catch",[33,859,860],{"class":91}," { ",[33,862,863],{"class":39},"\u002F* invalid URL *\u002F",[33,865,866],{"class":91}," }\n",[33,868,869,871,874,876],{"class":35,"line":369},[33,870,669],{"class":83},[33,872,873],{"class":113}," \"#\"",[33,875,602],{"class":91},[33,877,878],{"class":39},"\u002F\u002F fallback for dangerous or invalid URLs\n",[33,880,881],{"class":35,"line":375},[33,882,132],{"class":91},[33,884,885,887,889,891,893,895,897,899,902],{"class":35,"line":393},[33,886,442],{"class":91},[33,888,108],{"class":87},[33,890,92],{"class":91},[33,892,745],{"class":113},[33,894,748],{"class":91},[33,896,120],{"class":83},[33,898,772],{"class":87},[33,900,901],{"class":91},"(userInput);  ",[33,903,904],{"class":39},"\u002F\u002F \"#\" (blocked)\n",[33,906,907],{"class":35,"line":398},[33,908,71],{"emptyLinePlaceholder":70},[33,910,911],{"class":35,"line":414},[33,912,913],{"class":39},"\u002F\u002F ── Attribute injection (event handlers in attributes) ──\n",[33,915,916,918,921,923,926],{"class":35,"line":419},[33,917,565],{"class":83},[33,919,920],{"class":269}," userClass",[33,922,273],{"class":83},[33,924,925],{"class":113}," 'foo\" onmouseover=\"alert(1)'",[33,927,245],{"class":91},[33,929,930],{"class":35,"line":425},[33,931,932],{"class":39},"\u002F\u002F ❌ NAIVE — quote breakout\n",[33,934,935,937,939,941,944,946,949,952,955,957],{"class":35,"line":439},[33,936,457],{"class":91},[33,938,460],{"class":87},[33,940,92],{"class":91},[33,942,943],{"class":113},"\"class\"",[33,945,294],{"class":91},[33,947,948],{"class":113},"`custom ${",[33,950,951],{"class":91},"userClass",[33,953,954],{"class":113},"}`",[33,956,146],{"class":91},[33,958,959],{"class":39},"\u002F\u002F class=\"custom foo\" onmouseover=\"alert(1)\"\n",[33,961,962],{"class":35,"line":454},[33,963,964],{"class":39},"\u002F\u002F ✅ SAFE — use setAttribute (auto-escapes quotes in the value)\n",[33,966,967,969,971,973,975,978],{"class":35,"line":473},[33,968,457],{"class":91},[33,970,460],{"class":87},[33,972,92],{"class":91},[33,974,943],{"class":113},[33,976,977],{"class":91},", userClass);  ",[33,979,980],{"class":39},"\u002F\u002F quotes escaped, no breakout\n",[15,982,984],{"id":983},"csrf-cross-site-request-forgery","CSRF: Cross-Site Request Forgery",[20,986,987],{"language":22},[24,988,990],{"className":26,"code":989,"language":22,"meta":28,"style":28},"\u002F\u002F ── CSRF: attacker tricks a user's browser into sending a request to your site ──\n\u002F\u002F The browser includes cookies → the request appears authenticated.\n\u002F\u002F Attack vector: \u003Cimg src=\"https:\u002F\u002Fbank.com\u002Ftransfer?to=attacker&amount=1000\">\n\n\u002F\u002F ── Defense 1: CSRF tokens (synchronizer pattern) ──\n\u002F\u002F Server generates a random token, embeds it in the form, validates on POST.\n\u002F\u002F Attackers can't read the token (same-origin policy prevents cross-site reads).\n\n\u002F\u002F Frontend: include the token in requests\nconst csrfToken = document.querySelector('meta[name=\"csrf-token\"]').content;\nfetch(\"\u002Fapi\u002Ftransfer\", {\n    method: \"POST\",\n    headers: {\n        \"Content-Type\": \"application\u002Fjson\",\n        \"X-CSRF-Token\": csrfToken,  \u002F\u002F send the token in a header\n    },\n    body: JSON.stringify({ to: \"alice\", amount: 100 }),\n    credentials: \"same-origin\",  \u002F\u002F include cookies\n});\n\n\u002F\u002F ── Defense 2: SameSite cookies (prevent cross-site cookie transmission) ──\n\u002F\u002F Server sets: Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly\n\u002F\u002F SameSite=Strict: cookie never sent on cross-site requests (most secure)\n\u002F\u002F SameSite=Lax: cookie sent on top-level navigation (links), not on subrequests\n\u002F\u002F SameSite=None: cookie always sent (requires Secure — HTTPS only)\n\n\u002F\u002F ── Defense 3: custom header requirement (not achievable with plain forms) ──\n\u002F\u002F Require a custom header (X-Requested-With) — plain HTML forms can't set custom headers.\n\u002F\u002F Attackers can't send custom headers cross-origin without CORS permission.\nfetch(\"\u002Fapi\u002Fdata\", {\n    headers: { \"X-Requested-With\": \"XMLHttpRequest\" },  \u002F\u002F custom header\n    credentials: \"same-origin\",\n});\n",[30,991,992,997,1002,1007,1011,1016,1021,1026,1030,1035,1056,1069,1080,1085,1098,1109,1114,1143,1157,1162,1166,1171,1176,1181,1186,1191,1195,1200,1205,1210,1221,1240,1248],{"__ignoreMap":28},[33,993,994],{"class":35,"line":36},[33,995,996],{"class":39},"\u002F\u002F ── CSRF: attacker tricks a user's browser into sending a request to your site ──\n",[33,998,999],{"class":35,"line":43},[33,1000,1001],{"class":39},"\u002F\u002F The browser includes cookies → the request appears authenticated.\n",[33,1003,1004],{"class":35,"line":49},[33,1005,1006],{"class":39},"\u002F\u002F Attack vector: \u003Cimg src=\"https:\u002F\u002Fbank.com\u002Ftransfer?to=attacker&amount=1000\">\n",[33,1008,1009],{"class":35,"line":55},[33,1010,71],{"emptyLinePlaceholder":70},[33,1012,1013],{"class":35,"line":61},[33,1014,1015],{"class":39},"\u002F\u002F ── Defense 1: CSRF tokens (synchronizer pattern) ──\n",[33,1017,1018],{"class":35,"line":67},[33,1019,1020],{"class":39},"\u002F\u002F Server generates a random token, embeds it in the form, validates on POST.\n",[33,1022,1023],{"class":35,"line":74},[33,1024,1025],{"class":39},"\u002F\u002F Attackers can't read the token (same-origin policy prevents cross-site reads).\n",[33,1027,1028],{"class":35,"line":80},[33,1029,71],{"emptyLinePlaceholder":70},[33,1031,1032],{"class":35,"line":102},[33,1033,1034],{"class":39},"\u002F\u002F Frontend: include the token in requests\n",[33,1036,1037,1039,1042,1044,1046,1048,1050,1053],{"class":35,"line":129},[33,1038,565],{"class":83},[33,1040,1041],{"class":269}," csrfToken",[33,1043,273],{"class":83},[33,1045,640],{"class":91},[33,1047,108],{"class":87},[33,1049,92],{"class":91},[33,1051,1052],{"class":113},"'meta[name=\"csrf-token\"]'",[33,1054,1055],{"class":91},").content;\n",[33,1057,1058,1061,1063,1066],{"class":35,"line":135},[33,1059,1060],{"class":87},"fetch",[33,1062,92],{"class":91},[33,1064,1065],{"class":113},"\"\u002Fapi\u002Ftransfer\"",[33,1067,1068],{"class":91},", {\n",[33,1070,1071,1074,1077],{"class":35,"line":152},[33,1072,1073],{"class":91},"    method: ",[33,1075,1076],{"class":113},"\"POST\"",[33,1078,1079],{"class":91},",\n",[33,1081,1082],{"class":35,"line":157},[33,1083,1084],{"class":91},"    headers: {\n",[33,1086,1087,1090,1093,1096],{"class":35,"line":163},[33,1088,1089],{"class":113},"        \"Content-Type\"",[33,1091,1092],{"class":91},": ",[33,1094,1095],{"class":113},"\"application\u002Fjson\"",[33,1097,1079],{"class":91},[33,1099,1100,1103,1106],{"class":35,"line":177},[33,1101,1102],{"class":113},"        \"X-CSRF-Token\"",[33,1104,1105],{"class":91},": csrfToken,  ",[33,1107,1108],{"class":39},"\u002F\u002F send the token in a header\n",[33,1110,1111],{"class":35,"line":198},[33,1112,1113],{"class":91},"    },\n",[33,1115,1116,1119,1122,1125,1128,1131,1134,1137,1140],{"class":35,"line":203},[33,1117,1118],{"class":91},"    body: ",[33,1120,1121],{"class":269},"JSON",[33,1123,1124],{"class":91},".",[33,1126,1127],{"class":87},"stringify",[33,1129,1130],{"class":91},"({ to: ",[33,1132,1133],{"class":113},"\"alice\"",[33,1135,1136],{"class":91},", amount: ",[33,1138,1139],{"class":269},"100",[33,1141,1142],{"class":91}," }),\n",[33,1144,1145,1148,1151,1154],{"class":35,"line":219},[33,1146,1147],{"class":91},"    credentials: ",[33,1149,1150],{"class":113},"\"same-origin\"",[33,1152,1153],{"class":91},",  ",[33,1155,1156],{"class":39},"\u002F\u002F include cookies\n",[33,1158,1159],{"class":35,"line":224},[33,1160,1161],{"class":91},"});\n",[33,1163,1164],{"class":35,"line":230},[33,1165,71],{"emptyLinePlaceholder":70},[33,1167,1168],{"class":35,"line":248},[33,1169,1170],{"class":39},"\u002F\u002F ── Defense 2: SameSite cookies (prevent cross-site cookie transmission) ──\n",[33,1172,1173],{"class":35,"line":263},[33,1174,1175],{"class":39},"\u002F\u002F Server sets: Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly\n",[33,1177,1178],{"class":35,"line":285},[33,1179,1180],{"class":39},"\u002F\u002F SameSite=Strict: cookie never sent on cross-site requests (most secure)\n",[33,1182,1183],{"class":35,"line":328},[33,1184,1185],{"class":39},"\u002F\u002F SameSite=Lax: cookie sent on top-level navigation (links), not on subrequests\n",[33,1187,1188],{"class":35,"line":344},[33,1189,1190],{"class":39},"\u002F\u002F SameSite=None: cookie always sent (requires Secure — HTTPS only)\n",[33,1192,1193],{"class":35,"line":369},[33,1194,71],{"emptyLinePlaceholder":70},[33,1196,1197],{"class":35,"line":375},[33,1198,1199],{"class":39},"\u002F\u002F ── Defense 3: custom header requirement (not achievable with plain forms) ──\n",[33,1201,1202],{"class":35,"line":393},[33,1203,1204],{"class":39},"\u002F\u002F Require a custom header (X-Requested-With) — plain HTML forms can't set custom headers.\n",[33,1206,1207],{"class":35,"line":398},[33,1208,1209],{"class":39},"\u002F\u002F Attackers can't send custom headers cross-origin without CORS permission.\n",[33,1211,1212,1214,1216,1219],{"class":35,"line":414},[33,1213,1060],{"class":87},[33,1215,92],{"class":91},[33,1217,1218],{"class":113},"\"\u002Fapi\u002Fdata\"",[33,1220,1068],{"class":91},[33,1222,1223,1226,1229,1231,1234,1237],{"class":35,"line":419},[33,1224,1225],{"class":91},"    headers: { ",[33,1227,1228],{"class":113},"\"X-Requested-With\"",[33,1230,1092],{"class":91},[33,1232,1233],{"class":113},"\"XMLHttpRequest\"",[33,1235,1236],{"class":91}," },  ",[33,1238,1239],{"class":39},"\u002F\u002F custom header\n",[33,1241,1242,1244,1246],{"class":35,"line":425},[33,1243,1147],{"class":91},[33,1245,1150],{"class":113},[33,1247,1079],{"class":91},[33,1249,1250],{"class":35,"line":439},[33,1251,1161],{"class":91},[15,1253,1255],{"id":1254},"content-security-policy-csp","Content Security Policy (CSP)",[20,1257,1258],{"language":22},[24,1259,1261],{"className":26,"code":1260,"language":22,"meta":28,"style":28},"\u002F\u002F ── CSP: restrict what resources the browser is allowed to load ──\n\u002F\u002F Set via HTTP header or \u003Cmeta http-equiv=\"Content-Security-Policy\" content=\"...\">\n\n\u002F\u002F ── Strict CSP (prevents XSS, inline scripts, unauthorized connections) ──\nconst csp = [\n    \"default-src 'self'\",                    \u002F\u002F default: only same origin\n    \"script-src 'self'\",                     \u002F\u002F scripts: only same origin (no inline)\n    \"style-src 'self' 'unsafe-inline'\",      \u002F\u002F styles: same origin + inline (for CSS-in-JS)\n    \"img-src 'self' data: https:\",           \u002F\u002F images: same origin, data URIs, HTTPS\n    \"connect-src 'self' https:\u002F\u002Fapi.example.com\",  \u002F\u002F fetch\u002FXHR: same origin + API\n    \"font-src 'self' https:\u002F\u002Ffonts.gstatic.com\",   \u002F\u002F fonts: same origin + Google Fonts\n    \"frame-ancestors 'none'\",                \u002F\u002F prevent framing (clickjacking)\n    \"base-uri 'self'\",                       \u002F\u002F \u003Cbase> tag: only same origin\n    \"form-action 'self'\",                    \u002F\u002F form submissions: only same origin\n    \"object-src 'none'\",                     \u002F\u002F no \u003Cobject>\u002F\u003Cembed> (Flash, Java)\n    \"upgrade-insecure-requests\",             \u002F\u002F upgrade HTTP to HTTPS\n].join(\"; \");\n\n\u002F\u002F ── Nonce-based CSP (allow specific inline scripts) ──\n\u002F\u002F Server generates a unique nonce per request:\n\u002F\u002F Content-Security-Policy: script-src 'self' 'nonce-random123'\n\u002F\u002F \u003Cscript nonce=\"random123\">console.log(\"allowed\");\u003C\u002Fscript>\n\u002F\u002F The nonce changes per request — attackers can't guess it.\n\n\u002F\u002F ── Hash-based CSP (allow scripts by their content hash) ──\n\u002F\u002F script-src 'self' 'sha256-abc123...'\n\u002F\u002F Only scripts with the matching hash execute.\n\n\u002F\u002F ── Reporting (monitor violations without blocking) ──\n\u002F\u002F Content-Security-Policy-Report-Only: default-src 'self'; report-to \u002Fcsp-report\n\u002F\u002F Sends violation reports to \u002Fcsp-report (doesn't block — for testing before enforcing).\n",[30,1262,1263,1268,1273,1277,1282,1294,1305,1316,1327,1338,1348,1359,1370,1381,1391,1401,1412,1427,1431,1436,1441,1446,1451,1456,1460,1465,1470,1475,1479,1484,1489],{"__ignoreMap":28},[33,1264,1265],{"class":35,"line":36},[33,1266,1267],{"class":39},"\u002F\u002F ── CSP: restrict what resources the browser is allowed to load ──\n",[33,1269,1270],{"class":35,"line":43},[33,1271,1272],{"class":39},"\u002F\u002F Set via HTTP header or \u003Cmeta http-equiv=\"Content-Security-Policy\" content=\"...\">\n",[33,1274,1275],{"class":35,"line":49},[33,1276,71],{"emptyLinePlaceholder":70},[33,1278,1279],{"class":35,"line":55},[33,1280,1281],{"class":39},"\u002F\u002F ── Strict CSP (prevents XSS, inline scripts, unauthorized connections) ──\n",[33,1283,1284,1286,1289,1291],{"class":35,"line":61},[33,1285,565],{"class":83},[33,1287,1288],{"class":269}," csp",[33,1290,273],{"class":83},[33,1292,1293],{"class":91}," [\n",[33,1295,1296,1299,1302],{"class":35,"line":67},[33,1297,1298],{"class":113},"    \"default-src 'self'\"",[33,1300,1301],{"class":91},",                    ",[33,1303,1304],{"class":39},"\u002F\u002F default: only same origin\n",[33,1306,1307,1310,1313],{"class":35,"line":74},[33,1308,1309],{"class":113},"    \"script-src 'self'\"",[33,1311,1312],{"class":91},",                     ",[33,1314,1315],{"class":39},"\u002F\u002F scripts: only same origin (no inline)\n",[33,1317,1318,1321,1324],{"class":35,"line":80},[33,1319,1320],{"class":113},"    \"style-src 'self' 'unsafe-inline'\"",[33,1322,1323],{"class":91},",      ",[33,1325,1326],{"class":39},"\u002F\u002F styles: same origin + inline (for CSS-in-JS)\n",[33,1328,1329,1332,1335],{"class":35,"line":102},[33,1330,1331],{"class":113},"    \"img-src 'self' data: https:\"",[33,1333,1334],{"class":91},",           ",[33,1336,1337],{"class":39},"\u002F\u002F images: same origin, data URIs, HTTPS\n",[33,1339,1340,1343,1345],{"class":35,"line":129},[33,1341,1342],{"class":113},"    \"connect-src 'self' https:\u002F\u002Fapi.example.com\"",[33,1344,1153],{"class":91},[33,1346,1347],{"class":39},"\u002F\u002F fetch\u002FXHR: same origin + API\n",[33,1349,1350,1353,1356],{"class":35,"line":135},[33,1351,1352],{"class":113},"    \"font-src 'self' https:\u002F\u002Ffonts.gstatic.com\"",[33,1354,1355],{"class":91},",   ",[33,1357,1358],{"class":39},"\u002F\u002F fonts: same origin + Google Fonts\n",[33,1360,1361,1364,1367],{"class":35,"line":152},[33,1362,1363],{"class":113},"    \"frame-ancestors 'none'\"",[33,1365,1366],{"class":91},",                ",[33,1368,1369],{"class":39},"\u002F\u002F prevent framing (clickjacking)\n",[33,1371,1372,1375,1378],{"class":35,"line":157},[33,1373,1374],{"class":113},"    \"base-uri 'self'\"",[33,1376,1377],{"class":91},",                       ",[33,1379,1380],{"class":39},"\u002F\u002F \u003Cbase> tag: only same origin\n",[33,1382,1383,1386,1388],{"class":35,"line":163},[33,1384,1385],{"class":113},"    \"form-action 'self'\"",[33,1387,1301],{"class":91},[33,1389,1390],{"class":39},"\u002F\u002F form submissions: only same origin\n",[33,1392,1393,1396,1398],{"class":35,"line":177},[33,1394,1395],{"class":113},"    \"object-src 'none'\"",[33,1397,1312],{"class":91},[33,1399,1400],{"class":39},"\u002F\u002F no \u003Cobject>\u002F\u003Cembed> (Flash, Java)\n",[33,1402,1403,1406,1409],{"class":35,"line":198},[33,1404,1405],{"class":113},"    \"upgrade-insecure-requests\"",[33,1407,1408],{"class":91},",             ",[33,1410,1411],{"class":39},"\u002F\u002F upgrade HTTP to HTTPS\n",[33,1413,1414,1417,1420,1422,1425],{"class":35,"line":203},[33,1415,1416],{"class":91},"].",[33,1418,1419],{"class":87},"join",[33,1421,92],{"class":91},[33,1423,1424],{"class":113},"\"; \"",[33,1426,651],{"class":91},[33,1428,1429],{"class":35,"line":219},[33,1430,71],{"emptyLinePlaceholder":70},[33,1432,1433],{"class":35,"line":224},[33,1434,1435],{"class":39},"\u002F\u002F ── Nonce-based CSP (allow specific inline scripts) ──\n",[33,1437,1438],{"class":35,"line":230},[33,1439,1440],{"class":39},"\u002F\u002F Server generates a unique nonce per request:\n",[33,1442,1443],{"class":35,"line":248},[33,1444,1445],{"class":39},"\u002F\u002F Content-Security-Policy: script-src 'self' 'nonce-random123'\n",[33,1447,1448],{"class":35,"line":263},[33,1449,1450],{"class":39},"\u002F\u002F \u003Cscript nonce=\"random123\">console.log(\"allowed\");\u003C\u002Fscript>\n",[33,1452,1453],{"class":35,"line":285},[33,1454,1455],{"class":39},"\u002F\u002F The nonce changes per request — attackers can't guess it.\n",[33,1457,1458],{"class":35,"line":328},[33,1459,71],{"emptyLinePlaceholder":70},[33,1461,1462],{"class":35,"line":344},[33,1463,1464],{"class":39},"\u002F\u002F ── Hash-based CSP (allow scripts by their content hash) ──\n",[33,1466,1467],{"class":35,"line":369},[33,1468,1469],{"class":39},"\u002F\u002F script-src 'self' 'sha256-abc123...'\n",[33,1471,1472],{"class":35,"line":375},[33,1473,1474],{"class":39},"\u002F\u002F Only scripts with the matching hash execute.\n",[33,1476,1477],{"class":35,"line":393},[33,1478,71],{"emptyLinePlaceholder":70},[33,1480,1481],{"class":35,"line":398},[33,1482,1483],{"class":39},"\u002F\u002F ── Reporting (monitor violations without blocking) ──\n",[33,1485,1486],{"class":35,"line":414},[33,1487,1488],{"class":39},"\u002F\u002F Content-Security-Policy-Report-Only: default-src 'self'; report-to \u002Fcsp-report\n",[33,1490,1491],{"class":35,"line":419},[33,1492,1493],{"class":39},"\u002F\u002F Sends violation reports to \u002Fcsp-report (doesn't block — for testing before enforcing).\n",[15,1495,1497],{"id":1496},"prototype-pollution","Prototype Pollution",[20,1499,1500],{"language":22},[24,1501,1503],{"className":26,"code":1502,"language":22,"meta":28,"style":28},"\u002F\u002F ── Prototype pollution: attacker modifies Object.prototype ──\n\u002F\u002F All objects inherit from Object.prototype — polluting it affects every object.\n\n\u002F\u002F ❌ VULNERABLE — recursive merge can pollute the prototype\nfunction merge(target, source) {\n    for (const key in source) {\n        if (typeof source[key] === \"object\" && source[key] !== null) {\n            if (!target[key]) target[key] = {};\n            merge(target[key], source[key]);\n        } else {\n            target[key] = source[key];\n        }\n    }\n    return target;\n}\n\u002F\u002F Attack: merge({}, JSON.parse('{\"__proto__\": {\"isAdmin\": true}}'))\n\u002F\u002F After this, every new object has isAdmin: true → privilege escalation\nconst obj = {};\nconsole.log(obj.isAdmin);  \u002F\u002F true (polluted!) — every object now has isAdmin\n\n\u002F\u002F ✅ SAFE — block __proto__, constructor, prototype keys\nfunction safeMerge(target, source) {\n    for (const key in source) {\n        if (key === \"__proto__\" || key === \"constructor\" || key === \"prototype\") {\n            continue;  \u002F\u002F skip dangerous keys\n        }\n        if (typeof source[key] === \"object\" && source[key] !== null) {\n            target[key] = safeMerge(target[key] || {}, source[key]);\n        } else {\n            target[key] = source[key];\n        }\n    }\n    return target;\n}\n\n\u002F\u002F ✅ SAFER — use Object.create(null) (no prototype)\nconst map = Object.create(null);  \u002F\u002F no __proto__, no inherited properties\n\u002F\u002F Object.create(null) objects can't be polluted (they have no prototype chain)\n\n\u002F\u002F ✅ BEST — use Map instead of plain objects for key-value storage\nconst safeMap = new Map();\nsafeMap.set(\"key\", \"value\");  \u002F\u002F no prototype pollution risk\n",[30,1504,1505,1510,1515,1519,1524,1543,1562,1592,1610,1618,1628,1638,1642,1647,1654,1658,1663,1668,1679,1693,1697,1702,1719,1733,1766,1776,1780,1804,1821,1829,1837,1841,1845,1851,1855,1859,1864,1889,1894,1898,1904,1922],{"__ignoreMap":28},[33,1506,1507],{"class":35,"line":36},[33,1508,1509],{"class":39},"\u002F\u002F ── Prototype pollution: attacker modifies Object.prototype ──\n",[33,1511,1512],{"class":35,"line":43},[33,1513,1514],{"class":39},"\u002F\u002F All objects inherit from Object.prototype — polluting it affects every object.\n",[33,1516,1517],{"class":35,"line":49},[33,1518,71],{"emptyLinePlaceholder":70},[33,1520,1521],{"class":35,"line":55},[33,1522,1523],{"class":39},"\u002F\u002F ❌ VULNERABLE — recursive merge can pollute the prototype\n",[33,1525,1526,1528,1531,1533,1536,1538,1541],{"class":35,"line":61},[33,1527,84],{"class":83},[33,1529,1530],{"class":87}," merge",[33,1532,92],{"class":91},[33,1534,1535],{"class":95},"target",[33,1537,294],{"class":91},[33,1539,1540],{"class":95},"source",[33,1542,99],{"class":91},[33,1544,1545,1548,1551,1553,1556,1559],{"class":35,"line":67},[33,1546,1547],{"class":83},"    for",[33,1549,1550],{"class":91}," (",[33,1552,565],{"class":83},[33,1554,1555],{"class":269}," key",[33,1557,1558],{"class":83}," in",[33,1560,1561],{"class":91}," source) {\n",[33,1563,1564,1566,1568,1571,1574,1576,1579,1582,1584,1587,1590],{"class":35,"line":74},[33,1565,811],{"class":83},[33,1567,1550],{"class":91},[33,1569,1570],{"class":83},"typeof",[33,1572,1573],{"class":91}," source[key] ",[33,1575,817],{"class":83},[33,1577,1578],{"class":113}," \"object\"",[33,1580,1581],{"class":83}," &&",[33,1583,1573],{"class":91},[33,1585,1586],{"class":83},"!==",[33,1588,1589],{"class":269}," null",[33,1591,99],{"class":91},[33,1593,1594,1597,1599,1602,1605,1607],{"class":35,"line":80},[33,1595,1596],{"class":83},"            if",[33,1598,1550],{"class":91},[33,1600,1601],{"class":83},"!",[33,1603,1604],{"class":91},"target[key]) target[key] ",[33,1606,120],{"class":83},[33,1608,1609],{"class":91}," {};\n",[33,1611,1612,1615],{"class":35,"line":102},[33,1613,1614],{"class":87},"            merge",[33,1616,1617],{"class":91},"(target[key], source[key]);\n",[33,1619,1620,1623,1626],{"class":35,"line":129},[33,1621,1622],{"class":91},"        } ",[33,1624,1625],{"class":83},"else",[33,1627,787],{"class":91},[33,1629,1630,1633,1635],{"class":35,"line":135},[33,1631,1632],{"class":91},"            target[key] ",[33,1634,120],{"class":83},[33,1636,1637],{"class":91}," source[key];\n",[33,1639,1640],{"class":35,"line":152},[33,1641,849],{"class":91},[33,1643,1644],{"class":35,"line":157},[33,1645,1646],{"class":91},"    }\n",[33,1648,1649,1651],{"class":35,"line":163},[33,1650,669],{"class":83},[33,1652,1653],{"class":91}," target;\n",[33,1655,1656],{"class":35,"line":177},[33,1657,132],{"class":91},[33,1659,1660],{"class":35,"line":198},[33,1661,1662],{"class":39},"\u002F\u002F Attack: merge({}, JSON.parse('{\"__proto__\": {\"isAdmin\": true}}'))\n",[33,1664,1665],{"class":35,"line":203},[33,1666,1667],{"class":39},"\u002F\u002F After this, every new object has isAdmin: true → privilege escalation\n",[33,1669,1670,1672,1675,1677],{"class":35,"line":219},[33,1671,565],{"class":83},[33,1673,1674],{"class":269}," obj",[33,1676,273],{"class":83},[33,1678,1609],{"class":91},[33,1680,1681,1684,1687,1690],{"class":35,"line":224},[33,1682,1683],{"class":91},"console.",[33,1685,1686],{"class":87},"log",[33,1688,1689],{"class":91},"(obj.isAdmin);  ",[33,1691,1692],{"class":39},"\u002F\u002F true (polluted!) — every object now has isAdmin\n",[33,1694,1695],{"class":35,"line":230},[33,1696,71],{"emptyLinePlaceholder":70},[33,1698,1699],{"class":35,"line":248},[33,1700,1701],{"class":39},"\u002F\u002F ✅ SAFE — block __proto__, constructor, prototype keys\n",[33,1703,1704,1706,1709,1711,1713,1715,1717],{"class":35,"line":263},[33,1705,84],{"class":83},[33,1707,1708],{"class":87}," safeMerge",[33,1710,92],{"class":91},[33,1712,1535],{"class":95},[33,1714,294],{"class":91},[33,1716,1540],{"class":95},[33,1718,99],{"class":91},[33,1720,1721,1723,1725,1727,1729,1731],{"class":35,"line":285},[33,1722,1547],{"class":83},[33,1724,1550],{"class":91},[33,1726,565],{"class":83},[33,1728,1555],{"class":269},[33,1730,1558],{"class":83},[33,1732,1561],{"class":91},[33,1734,1735,1737,1740,1742,1745,1747,1750,1752,1755,1757,1759,1761,1764],{"class":35,"line":328},[33,1736,811],{"class":83},[33,1738,1739],{"class":91}," (key ",[33,1741,817],{"class":83},[33,1743,1744],{"class":113}," \"__proto__\"",[33,1746,823],{"class":83},[33,1748,1749],{"class":91}," key ",[33,1751,817],{"class":83},[33,1753,1754],{"class":113}," \"constructor\"",[33,1756,823],{"class":83},[33,1758,1749],{"class":91},[33,1760,817],{"class":83},[33,1762,1763],{"class":113}," \"prototype\"",[33,1765,99],{"class":91},[33,1767,1768,1771,1773],{"class":35,"line":344},[33,1769,1770],{"class":83},"            continue",[33,1772,602],{"class":91},[33,1774,1775],{"class":39},"\u002F\u002F skip dangerous keys\n",[33,1777,1778],{"class":35,"line":369},[33,1779,849],{"class":91},[33,1781,1782,1784,1786,1788,1790,1792,1794,1796,1798,1800,1802],{"class":35,"line":375},[33,1783,811],{"class":83},[33,1785,1550],{"class":91},[33,1787,1570],{"class":83},[33,1789,1573],{"class":91},[33,1791,817],{"class":83},[33,1793,1578],{"class":113},[33,1795,1581],{"class":83},[33,1797,1573],{"class":91},[33,1799,1586],{"class":83},[33,1801,1589],{"class":269},[33,1803,99],{"class":91},[33,1805,1806,1808,1810,1812,1815,1818],{"class":35,"line":393},[33,1807,1632],{"class":91},[33,1809,120],{"class":83},[33,1811,1708],{"class":87},[33,1813,1814],{"class":91},"(target[key] ",[33,1816,1817],{"class":83},"||",[33,1819,1820],{"class":91}," {}, source[key]);\n",[33,1822,1823,1825,1827],{"class":35,"line":398},[33,1824,1622],{"class":91},[33,1826,1625],{"class":83},[33,1828,787],{"class":91},[33,1830,1831,1833,1835],{"class":35,"line":414},[33,1832,1632],{"class":91},[33,1834,120],{"class":83},[33,1836,1637],{"class":91},[33,1838,1839],{"class":35,"line":419},[33,1840,849],{"class":91},[33,1842,1843],{"class":35,"line":425},[33,1844,1646],{"class":91},[33,1846,1847,1849],{"class":35,"line":439},[33,1848,669],{"class":83},[33,1850,1653],{"class":91},[33,1852,1853],{"class":35,"line":454},[33,1854,132],{"class":91},[33,1856,1857],{"class":35,"line":473},[33,1858,71],{"emptyLinePlaceholder":70},[33,1860,1861],{"class":35,"line":486},[33,1862,1863],{"class":39},"\u002F\u002F ✅ SAFER — use Object.create(null) (no prototype)\n",[33,1865,1866,1868,1871,1873,1876,1879,1881,1884,1886],{"class":35,"line":498},[33,1867,565],{"class":83},[33,1869,1870],{"class":269}," map",[33,1872,273],{"class":83},[33,1874,1875],{"class":91}," Object.",[33,1877,1878],{"class":87},"create",[33,1880,92],{"class":91},[33,1882,1883],{"class":269},"null",[33,1885,146],{"class":91},[33,1887,1888],{"class":39},"\u002F\u002F no __proto__, no inherited properties\n",[33,1890,1891],{"class":35,"line":516},[33,1892,1893],{"class":39},"\u002F\u002F Object.create(null) objects can't be polluted (they have no prototype chain)\n",[33,1895,1896],{"class":35,"line":532},[33,1897,71],{"emptyLinePlaceholder":70},[33,1899,1901],{"class":35,"line":1900},40,[33,1902,1903],{"class":39},"\u002F\u002F ✅ BEST — use Map instead of plain objects for key-value storage\n",[33,1905,1907,1909,1912,1914,1916,1919],{"class":35,"line":1906},41,[33,1908,565],{"class":83},[33,1910,1911],{"class":269}," safeMap",[33,1913,273],{"class":83},[33,1915,800],{"class":83},[33,1917,1918],{"class":87}," Map",[33,1920,1921],{"class":91},"();\n",[33,1923,1925,1928,1931,1933,1936,1938,1941,1943],{"class":35,"line":1924},42,[33,1926,1927],{"class":91},"safeMap.",[33,1929,1930],{"class":87},"set",[33,1932,92],{"class":91},[33,1934,1935],{"class":113},"\"key\"",[33,1937,294],{"class":91},[33,1939,1940],{"class":113},"\"value\"",[33,1942,146],{"class":91},[33,1944,1945],{"class":39},"\u002F\u002F no prototype pollution risk\n",[15,1947,1949],{"id":1948},"input-validation-and-sanitization","Input Validation and Sanitization",[20,1951,1952],{"language":22},[24,1953,1955],{"className":26,"code":1954,"language":22,"meta":28,"style":28},"\u002F\u002F ── Validate input early and strictly (never trust client-side validation alone) ──\n\u002F\u002F Client-side: UX feedback. Server-side: security enforcement.\n\n\u002F\u002F ── Schema validation with a validator (e.g., Zod, Joi, yup) ──\n\u002F\u002F Using Zod:\nimport { z } from \"zod\";\n\nconst UserSchema = z.object({\n    email: z.string().email(),  \u002F\u002F must be a valid email\n    age: z.number().int().min(0).max(150),  \u002F\u002F integer, 0-150\n    name: z.string().min(1).max(100),  \u002F\u002F 1-100 chars\n    role: z.enum([\"user\", \"admin\"]),  \u002F\u002F only these values\n    website: z.string().url().optional(),  \u002F\u002F valid URL or undefined\n});\n\n\u002F\u002F Parse and validate (throws on invalid input)\nconst user = UserSchema.parse(request.body);  \u002F\u002F throws ZodError if invalid\n\u002F\u002F Safe parse (returns success\u002Ferror instead of throwing)\nconst result = UserSchema.safeParse(request.body);\nif (result.success) {\n    console.log(result.data);  \u002F\u002F typed and validated\n} else {\n    console.log(result.error.issues);  \u002F\u002F validation errors\n}\n\n\u002F\u002F ── SQL injection prevention (use parameterized queries) ──\n\u002F\u002F ❌ NAIVE — string concatenation (SQL injection)\nconst query = `SELECT * FROM users WHERE name = '${userName}'`;\n\u002F\u002F Attack: userName = \"'; DROP TABLE users; --\" → SQL injection\n\n\u002F\u002F ✅ SAFE — parameterized queries (database escapes the values)\nconst safeQuery = \"SELECT * FROM users WHERE name = ?\";\ndb.execute(safeQuery, [userName]);  \u002F\u002F userName is escaped by the driver\n",[30,1956,1957,1962,1967,1971,1976,1981,1995,1999,2017,2037,2076,2105,2130,2151,2155,2159,2164,2185,2190,2207,2215,2228,2237,2249,2253,2257,2262,2267,2286,2291,2295,2300,2314],{"__ignoreMap":28},[33,1958,1959],{"class":35,"line":36},[33,1960,1961],{"class":39},"\u002F\u002F ── Validate input early and strictly (never trust client-side validation alone) ──\n",[33,1963,1964],{"class":35,"line":43},[33,1965,1966],{"class":39},"\u002F\u002F Client-side: UX feedback. Server-side: security enforcement.\n",[33,1968,1969],{"class":35,"line":49},[33,1970,71],{"emptyLinePlaceholder":70},[33,1972,1973],{"class":35,"line":55},[33,1974,1975],{"class":39},"\u002F\u002F ── Schema validation with a validator (e.g., Zod, Joi, yup) ──\n",[33,1977,1978],{"class":35,"line":61},[33,1979,1980],{"class":39},"\u002F\u002F Using Zod:\n",[33,1982,1983,1985,1988,1990,1993],{"class":35,"line":67},[33,1984,233],{"class":83},[33,1986,1987],{"class":91}," { z } ",[33,1989,239],{"class":83},[33,1991,1992],{"class":113}," \"zod\"",[33,1994,245],{"class":91},[33,1996,1997],{"class":35,"line":74},[33,1998,71],{"emptyLinePlaceholder":70},[33,2000,2001,2003,2006,2008,2011,2014],{"class":35,"line":80},[33,2002,565],{"class":83},[33,2004,2005],{"class":269}," UserSchema",[33,2007,273],{"class":83},[33,2009,2010],{"class":91}," z.",[33,2012,2013],{"class":87},"object",[33,2015,2016],{"class":91},"({\n",[33,2018,2019,2022,2025,2028,2031,2034],{"class":35,"line":102},[33,2020,2021],{"class":91},"    email: z.",[33,2023,2024],{"class":87},"string",[33,2026,2027],{"class":91},"().",[33,2029,2030],{"class":87},"email",[33,2032,2033],{"class":91},"(),  ",[33,2035,2036],{"class":39},"\u002F\u002F must be a valid email\n",[33,2038,2039,2042,2045,2047,2050,2052,2055,2057,2059,2062,2065,2067,2070,2073],{"class":35,"line":129},[33,2040,2041],{"class":91},"    age: z.",[33,2043,2044],{"class":87},"number",[33,2046,2027],{"class":91},[33,2048,2049],{"class":87},"int",[33,2051,2027],{"class":91},[33,2053,2054],{"class":87},"min",[33,2056,92],{"class":91},[33,2058,507],{"class":269},[33,2060,2061],{"class":91},").",[33,2063,2064],{"class":87},"max",[33,2066,92],{"class":91},[33,2068,2069],{"class":269},"150",[33,2071,2072],{"class":91},"),  ",[33,2074,2075],{"class":39},"\u002F\u002F integer, 0-150\n",[33,2077,2078,2081,2083,2085,2087,2089,2092,2094,2096,2098,2100,2102],{"class":35,"line":135},[33,2079,2080],{"class":91},"    name: z.",[33,2082,2024],{"class":87},[33,2084,2027],{"class":91},[33,2086,2054],{"class":87},[33,2088,92],{"class":91},[33,2090,2091],{"class":269},"1",[33,2093,2061],{"class":91},[33,2095,2064],{"class":87},[33,2097,92],{"class":91},[33,2099,1139],{"class":269},[33,2101,2072],{"class":91},[33,2103,2104],{"class":39},"\u002F\u002F 1-100 chars\n",[33,2106,2107,2110,2113,2116,2119,2121,2124,2127],{"class":35,"line":152},[33,2108,2109],{"class":91},"    role: z.",[33,2111,2112],{"class":87},"enum",[33,2114,2115],{"class":91},"([",[33,2117,2118],{"class":113},"\"user\"",[33,2120,294],{"class":91},[33,2122,2123],{"class":113},"\"admin\"",[33,2125,2126],{"class":91},"]),  ",[33,2128,2129],{"class":39},"\u002F\u002F only these values\n",[33,2131,2132,2135,2137,2139,2141,2143,2146,2148],{"class":35,"line":157},[33,2133,2134],{"class":91},"    website: z.",[33,2136,2024],{"class":87},[33,2138,2027],{"class":91},[33,2140,777],{"class":87},[33,2142,2027],{"class":91},[33,2144,2145],{"class":87},"optional",[33,2147,2033],{"class":91},[33,2149,2150],{"class":39},"\u002F\u002F valid URL or undefined\n",[33,2152,2153],{"class":35,"line":163},[33,2154,1161],{"class":91},[33,2156,2157],{"class":35,"line":177},[33,2158,71],{"emptyLinePlaceholder":70},[33,2160,2161],{"class":35,"line":198},[33,2162,2163],{"class":39},"\u002F\u002F Parse and validate (throws on invalid input)\n",[33,2165,2166,2168,2171,2173,2176,2179,2182],{"class":35,"line":203},[33,2167,565],{"class":83},[33,2169,2170],{"class":269}," user",[33,2172,273],{"class":83},[33,2174,2175],{"class":91}," UserSchema.",[33,2177,2178],{"class":87},"parse",[33,2180,2181],{"class":91},"(request.body);  ",[33,2183,2184],{"class":39},"\u002F\u002F throws ZodError if invalid\n",[33,2186,2187],{"class":35,"line":219},[33,2188,2189],{"class":39},"\u002F\u002F Safe parse (returns success\u002Ferror instead of throwing)\n",[33,2191,2192,2194,2197,2199,2201,2204],{"class":35,"line":224},[33,2193,565],{"class":83},[33,2195,2196],{"class":269}," result",[33,2198,273],{"class":83},[33,2200,2175],{"class":91},[33,2202,2203],{"class":87},"safeParse",[33,2205,2206],{"class":91},"(request.body);\n",[33,2208,2209,2212],{"class":35,"line":230},[33,2210,2211],{"class":83},"if",[33,2213,2214],{"class":91}," (result.success) {\n",[33,2216,2217,2220,2222,2225],{"class":35,"line":248},[33,2218,2219],{"class":91},"    console.",[33,2221,1686],{"class":87},[33,2223,2224],{"class":91},"(result.data);  ",[33,2226,2227],{"class":39},"\u002F\u002F typed and validated\n",[33,2229,2230,2233,2235],{"class":35,"line":263},[33,2231,2232],{"class":91},"} ",[33,2234,1625],{"class":83},[33,2236,787],{"class":91},[33,2238,2239,2241,2243,2246],{"class":35,"line":285},[33,2240,2219],{"class":91},[33,2242,1686],{"class":87},[33,2244,2245],{"class":91},"(result.error.issues);  ",[33,2247,2248],{"class":39},"\u002F\u002F validation errors\n",[33,2250,2251],{"class":35,"line":328},[33,2252,132],{"class":91},[33,2254,2255],{"class":35,"line":344},[33,2256,71],{"emptyLinePlaceholder":70},[33,2258,2259],{"class":35,"line":369},[33,2260,2261],{"class":39},"\u002F\u002F ── SQL injection prevention (use parameterized queries) ──\n",[33,2263,2264],{"class":35,"line":375},[33,2265,2266],{"class":39},"\u002F\u002F ❌ NAIVE — string concatenation (SQL injection)\n",[33,2268,2269,2271,2274,2276,2279,2281,2284],{"class":35,"line":393},[33,2270,565],{"class":83},[33,2272,2273],{"class":269}," query",[33,2275,273],{"class":83},[33,2277,2278],{"class":113}," `SELECT * FROM users WHERE name = '${",[33,2280,596],{"class":91},[33,2282,2283],{"class":113},"}'`",[33,2285,245],{"class":91},[33,2287,2288],{"class":35,"line":398},[33,2289,2290],{"class":39},"\u002F\u002F Attack: userName = \"'; DROP TABLE users; --\" → SQL injection\n",[33,2292,2293],{"class":35,"line":414},[33,2294,71],{"emptyLinePlaceholder":70},[33,2296,2297],{"class":35,"line":419},[33,2298,2299],{"class":39},"\u002F\u002F ✅ SAFE — parameterized queries (database escapes the values)\n",[33,2301,2302,2304,2307,2309,2312],{"class":35,"line":425},[33,2303,565],{"class":83},[33,2305,2306],{"class":269}," safeQuery",[33,2308,273],{"class":83},[33,2310,2311],{"class":113}," \"SELECT * FROM users WHERE name = ?\"",[33,2313,245],{"class":91},[33,2315,2316,2319,2322,2325],{"class":35,"line":439},[33,2317,2318],{"class":91},"db.",[33,2320,2321],{"class":87},"execute",[33,2323,2324],{"class":91},"(safeQuery, [userName]);  ",[33,2326,2327],{"class":39},"\u002F\u002F userName is escaped by the driver\n",[15,2329,2331],{"id":2330},"subresource-integrity-sri","Subresource Integrity (SRI)",[20,2333,2334],{"language":258},[24,2335,2338],{"className":2336,"code":2337,"language":258,"meta":28,"style":28},"language-html shiki shiki-themes github-light github-dark","\u003C!-- ── SRI: verify the integrity of external scripts\u002Fstyles ── -->\n\u003C!-- If the CDN is compromised and the script changes, the browser won't execute it -->\n\u003Cscript\n  src=\"https:\u002F\u002Fcdn.example.com\u002Flibrary.js\"\n  integrity=\"sha384-abc123...\"\n  crossorigin=\"anonymous\">\n\u003C\u002Fscript>\n\u003C!-- The hash must match the file content. If it doesn't, the browser blocks execution. -->\n\u003C!-- Generate: openssl dgst -sha384 -binary library.js | openssl base64 -A -->\n",[30,2339,2340,2345,2350,2359,2369,2379,2392,2402,2407],{"__ignoreMap":28},[33,2341,2342],{"class":35,"line":36},[33,2343,2344],{"class":39},"\u003C!-- ── SRI: verify the integrity of external scripts\u002Fstyles ── -->\n",[33,2346,2347],{"class":35,"line":43},[33,2348,2349],{"class":39},"\u003C!-- If the CDN is compromised and the script changes, the browser won't execute it -->\n",[33,2351,2352,2355],{"class":35,"line":49},[33,2353,2354],{"class":91},"\u003C",[33,2356,2358],{"class":2357},"sk71V","script\n",[33,2360,2361,2364,2366],{"class":35,"line":55},[33,2362,2363],{"class":87},"  src",[33,2365,120],{"class":91},[33,2367,2368],{"class":113},"\"https:\u002F\u002Fcdn.example.com\u002Flibrary.js\"\n",[33,2370,2371,2374,2376],{"class":35,"line":61},[33,2372,2373],{"class":87},"  integrity",[33,2375,120],{"class":91},[33,2377,2378],{"class":113},"\"sha384-abc123...\"\n",[33,2380,2381,2384,2386,2389],{"class":35,"line":67},[33,2382,2383],{"class":87},"  crossorigin",[33,2385,120],{"class":91},[33,2387,2388],{"class":113},"\"anonymous\"",[33,2390,2391],{"class":91},">\n",[33,2393,2394,2397,2400],{"class":35,"line":74},[33,2395,2396],{"class":91},"\u003C\u002F",[33,2398,2399],{"class":2357},"script",[33,2401,2391],{"class":91},[33,2403,2404],{"class":35,"line":80},[33,2405,2406],{"class":39},"\u003C!-- The hash must match the file content. If it doesn't, the browser blocks execution. -->\n",[33,2408,2409],{"class":35,"line":102},[33,2410,2411],{"class":39},"\u003C!-- Generate: openssl dgst -sha384 -binary library.js | openssl base64 -A -->\n",[15,2413,2415],{"id":2414},"tips-tricks","💡 Tips & Tricks",[20,2417,2418],{"language":22},[24,2419,2421],{"className":26,"code":2420,"language":22,"meta":28,"style":28},"\u002F\u002F ── HttpOnly cookies: prevent JS from reading cookies (XSS can't steal them) ──\n\u002F\u002F Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict\n\u002F\u002F document.cookie → can't read \"session\" (HttpOnly) → XSS can't exfiltrate it\n\n\u002F\u002F ── Use `textContent` by default, `innerHTML` only with sanitization ──\n\u002F\u002F textContent: safe (no HTML parsing), slightly faster\n\u002F\u002F innerHTML: only with DOMPurify or trusted content\n\n\u002F\u002F ── Strict Transport Security (HSTS): force HTTPS ──\n\u002F\u002F Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n\u002F\u002F Prevents SSL stripping attacks (browser always uses HTTPS for this domain)\n\n\u002F\u002F ── Use `rel=\"noopener noreferrer\"` on target=\"_blank\" links ──\n\u002F\u002F \u003Ca href=\"https:\u002F\u002Fexternal.com\" target=\"_blank\" rel=\"noopener noreferrer\">\n\u002F\u002F Without noopener: the new tab can access window.opener (reverse tabnabbing)\n\n\u002F\u002F ── JSON hijacking prevention (X-Content-Type-Options: nosniff) ──\n\u002F\u002F Set: X-Content-Type-Options: nosniff\n\u002F\u002F Prevents the browser from MIME-sniffing (treating JSON as HTML\u002FJS)\n",[30,2422,2423,2428,2433,2438,2442,2447,2452,2457,2461,2466,2471,2476,2480,2485,2490,2495,2499,2504,2509],{"__ignoreMap":28},[33,2424,2425],{"class":35,"line":36},[33,2426,2427],{"class":39},"\u002F\u002F ── HttpOnly cookies: prevent JS from reading cookies (XSS can't steal them) ──\n",[33,2429,2430],{"class":35,"line":43},[33,2431,2432],{"class":39},"\u002F\u002F Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict\n",[33,2434,2435],{"class":35,"line":49},[33,2436,2437],{"class":39},"\u002F\u002F document.cookie → can't read \"session\" (HttpOnly) → XSS can't exfiltrate it\n",[33,2439,2440],{"class":35,"line":55},[33,2441,71],{"emptyLinePlaceholder":70},[33,2443,2444],{"class":35,"line":61},[33,2445,2446],{"class":39},"\u002F\u002F ── Use `textContent` by default, `innerHTML` only with sanitization ──\n",[33,2448,2449],{"class":35,"line":67},[33,2450,2451],{"class":39},"\u002F\u002F textContent: safe (no HTML parsing), slightly faster\n",[33,2453,2454],{"class":35,"line":74},[33,2455,2456],{"class":39},"\u002F\u002F innerHTML: only with DOMPurify or trusted content\n",[33,2458,2459],{"class":35,"line":80},[33,2460,71],{"emptyLinePlaceholder":70},[33,2462,2463],{"class":35,"line":102},[33,2464,2465],{"class":39},"\u002F\u002F ── Strict Transport Security (HSTS): force HTTPS ──\n",[33,2467,2468],{"class":35,"line":129},[33,2469,2470],{"class":39},"\u002F\u002F Strict-Transport-Security: max-age=31536000; includeSubDomains; preload\n",[33,2472,2473],{"class":35,"line":135},[33,2474,2475],{"class":39},"\u002F\u002F Prevents SSL stripping attacks (browser always uses HTTPS for this domain)\n",[33,2477,2478],{"class":35,"line":152},[33,2479,71],{"emptyLinePlaceholder":70},[33,2481,2482],{"class":35,"line":157},[33,2483,2484],{"class":39},"\u002F\u002F ── Use `rel=\"noopener noreferrer\"` on target=\"_blank\" links ──\n",[33,2486,2487],{"class":35,"line":163},[33,2488,2489],{"class":39},"\u002F\u002F \u003Ca href=\"https:\u002F\u002Fexternal.com\" target=\"_blank\" rel=\"noopener noreferrer\">\n",[33,2491,2492],{"class":35,"line":177},[33,2493,2494],{"class":39},"\u002F\u002F Without noopener: the new tab can access window.opener (reverse tabnabbing)\n",[33,2496,2497],{"class":35,"line":198},[33,2498,71],{"emptyLinePlaceholder":70},[33,2500,2501],{"class":35,"line":203},[33,2502,2503],{"class":39},"\u002F\u002F ── JSON hijacking prevention (X-Content-Type-Options: nosniff) ──\n",[33,2505,2506],{"class":35,"line":219},[33,2507,2508],{"class":39},"\u002F\u002F Set: X-Content-Type-Options: nosniff\n",[33,2510,2511],{"class":35,"line":224},[33,2512,2513],{"class":39},"\u002F\u002F Prevents the browser from MIME-sniffing (treating JSON as HTML\u002FJS)\n",[15,2515,2517],{"id":2516},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[20,2519,2520],{"language":22},[24,2521,2523],{"className":26,"code":2522,"language":22,"meta":28,"style":28},"\u002F\u002F ── `textContent` and `innerHTML` differ on script tags ──\nconst script = document.createElement(\"script\");\nscript.textContent = 'alert(\"executes\")';  \u002F\u002F ❌ DOES execute when inserted\ndocument.body.appendChild(script);  \u002F\u002F the script runs\n\n\u002F\u002F ── Sanitization is context-dependent ──\n\u002F\u002F DOMPurify sanitizes for HTML context. For URL context, validate the scheme.\n\u002F\u002F For JS string context, escape quotes and backslashes.\n\u002F\u002F No single sanitizer works in all contexts — use the right tool for each.\n\n\u002F\u002F ── `javascript:` URLs can bypass naive scheme checks ──\n\u002F\u002F \"java\\tscript:alert(1)\" → tab character bypasses \u002Fjavascript:\u002F regex check\n\u002F\u002F Use new URL() and check protocol (handles encoding correctly)\n\n\u002F\u002F ── Prototype pollution via JSON ──\n\u002F\u002F JSON.parse('{\"__proto__\": {\"x\": 1}}') → __proto__ is a key, not the prototype\n\u002F\u002F But if you merge this into an object with a recursive merge, it can pollute.\n\n\u002F\u002F ── `Object.freeze` doesn't prevent prototype pollution ──\n\u002F\u002F Object.freeze(obj) prevents changes to obj's own properties,\n\u002F\u002F but doesn't prevent changes to Object.prototype (the prototype).\n\n\u002F\u002F ── CSP doesn't prevent all XSS (only limits the damage) ──\n\u002F\u002F 'unsafe-inline' allows inline scripts (still vulnerable to inline XSS)\n\u002F\u002F Use nonces or hashes instead of 'unsafe-inline' for strict CSP.\n",[30,2524,2525,2530,2550,2565,2579,2583,2588,2593,2598,2603,2607,2612,2617,2622,2626,2631,2636,2641,2645,2650,2655,2660,2664,2669,2674],{"__ignoreMap":28},[33,2526,2527],{"class":35,"line":36},[33,2528,2529],{"class":39},"\u002F\u002F ── `textContent` and `innerHTML` differ on script tags ──\n",[33,2531,2532,2534,2537,2539,2541,2543,2545,2548],{"class":35,"line":43},[33,2533,565],{"class":83},[33,2535,2536],{"class":269}," script",[33,2538,273],{"class":83},[33,2540,640],{"class":91},[33,2542,643],{"class":87},[33,2544,92],{"class":91},[33,2546,2547],{"class":113},"\"script\"",[33,2549,651],{"class":91},[33,2551,2552,2555,2557,2560,2562],{"class":35,"line":49},[33,2553,2554],{"class":91},"script.textContent ",[33,2556,120],{"class":83},[33,2558,2559],{"class":113}," 'alert(\"executes\")'",[33,2561,602],{"class":91},[33,2563,2564],{"class":39},"\u002F\u002F ❌ DOES execute when inserted\n",[33,2566,2567,2570,2573,2576],{"class":35,"line":55},[33,2568,2569],{"class":91},"document.body.",[33,2571,2572],{"class":87},"appendChild",[33,2574,2575],{"class":91},"(script);  ",[33,2577,2578],{"class":39},"\u002F\u002F the script runs\n",[33,2580,2581],{"class":35,"line":61},[33,2582,71],{"emptyLinePlaceholder":70},[33,2584,2585],{"class":35,"line":67},[33,2586,2587],{"class":39},"\u002F\u002F ── Sanitization is context-dependent ──\n",[33,2589,2590],{"class":35,"line":74},[33,2591,2592],{"class":39},"\u002F\u002F DOMPurify sanitizes for HTML context. For URL context, validate the scheme.\n",[33,2594,2595],{"class":35,"line":80},[33,2596,2597],{"class":39},"\u002F\u002F For JS string context, escape quotes and backslashes.\n",[33,2599,2600],{"class":35,"line":102},[33,2601,2602],{"class":39},"\u002F\u002F No single sanitizer works in all contexts — use the right tool for each.\n",[33,2604,2605],{"class":35,"line":129},[33,2606,71],{"emptyLinePlaceholder":70},[33,2608,2609],{"class":35,"line":135},[33,2610,2611],{"class":39},"\u002F\u002F ── `javascript:` URLs can bypass naive scheme checks ──\n",[33,2613,2614],{"class":35,"line":152},[33,2615,2616],{"class":39},"\u002F\u002F \"java\\tscript:alert(1)\" → tab character bypasses \u002Fjavascript:\u002F regex check\n",[33,2618,2619],{"class":35,"line":157},[33,2620,2621],{"class":39},"\u002F\u002F Use new URL() and check protocol (handles encoding correctly)\n",[33,2623,2624],{"class":35,"line":163},[33,2625,71],{"emptyLinePlaceholder":70},[33,2627,2628],{"class":35,"line":177},[33,2629,2630],{"class":39},"\u002F\u002F ── Prototype pollution via JSON ──\n",[33,2632,2633],{"class":35,"line":198},[33,2634,2635],{"class":39},"\u002F\u002F JSON.parse('{\"__proto__\": {\"x\": 1}}') → __proto__ is a key, not the prototype\n",[33,2637,2638],{"class":35,"line":203},[33,2639,2640],{"class":39},"\u002F\u002F But if you merge this into an object with a recursive merge, it can pollute.\n",[33,2642,2643],{"class":35,"line":219},[33,2644,71],{"emptyLinePlaceholder":70},[33,2646,2647],{"class":35,"line":224},[33,2648,2649],{"class":39},"\u002F\u002F ── `Object.freeze` doesn't prevent prototype pollution ──\n",[33,2651,2652],{"class":35,"line":230},[33,2653,2654],{"class":39},"\u002F\u002F Object.freeze(obj) prevents changes to obj's own properties,\n",[33,2656,2657],{"class":35,"line":248},[33,2658,2659],{"class":39},"\u002F\u002F but doesn't prevent changes to Object.prototype (the prototype).\n",[33,2661,2662],{"class":35,"line":263},[33,2663,71],{"emptyLinePlaceholder":70},[33,2665,2666],{"class":35,"line":285},[33,2667,2668],{"class":39},"\u002F\u002F ── CSP doesn't prevent all XSS (only limits the damage) ──\n",[33,2670,2671],{"class":35,"line":328},[33,2672,2673],{"class":39},"\u002F\u002F 'unsafe-inline' allows inline scripts (still vulnerable to inline XSS)\n",[33,2675,2676],{"class":35,"line":344},[33,2677,2678],{"class":39},"\u002F\u002F Use nonces or hashes instead of 'unsafe-inline' for strict CSP.\n",[15,2680,2682],{"id":2681},"quick-quiz","🧠 Quick Quiz",[2684,2685,2686],"p",{},"Why is this code vulnerable, and how do you fix it?",[20,2688,2689],{"language":22},[24,2690,2692],{"className":26,"code":2691,"language":22,"meta":28,"style":28},"function highlightSearch(query) {\n    const results = document.querySelector(\"#results\");\n    results.innerHTML = `Found: ${items\n        .filter(item => item.name.includes(query))\n        .map(item => `\u003Cli>${item.name}\u003C\u002Fli>`)\n        .join(\"\")}`;\n}\n",[30,2693,2694,2708,2728,2741,2777,2805,2822],{"__ignoreMap":28},[33,2695,2696,2698,2701,2703,2706],{"class":35,"line":36},[33,2697,84],{"class":83},[33,2699,2700],{"class":87}," highlightSearch",[33,2702,92],{"class":91},[33,2704,2705],{"class":95},"query",[33,2707,99],{"class":91},[33,2709,2710,2712,2715,2717,2719,2721,2723,2726],{"class":35,"line":43},[33,2711,266],{"class":83},[33,2713,2714],{"class":269}," results",[33,2716,273],{"class":83},[33,2718,640],{"class":91},[33,2720,108],{"class":87},[33,2722,92],{"class":91},[33,2724,2725],{"class":113},"\"#results\"",[33,2727,651],{"class":91},[33,2729,2730,2733,2735,2738],{"class":35,"line":49},[33,2731,2732],{"class":91},"    results.innerHTML ",[33,2734,120],{"class":83},[33,2736,2737],{"class":113}," `Found: ${",[33,2739,2740],{"class":91},"items\n",[33,2742,2743,2746,2749,2751,2754,2757,2760,2762,2765,2767,2770,2772,2774],{"class":35,"line":55},[33,2744,2745],{"class":113},"        .",[33,2747,2748],{"class":87},"filter",[33,2750,92],{"class":113},[33,2752,2753],{"class":269},"item",[33,2755,2756],{"class":83}," =>",[33,2758,2759],{"class":91}," item",[33,2761,1124],{"class":113},[33,2763,2764],{"class":91},"name",[33,2766,1124],{"class":113},[33,2768,2769],{"class":87},"includes",[33,2771,92],{"class":113},[33,2773,2705],{"class":91},[33,2775,2776],{"class":113},"))\n",[33,2778,2779,2781,2784,2786,2788,2790,2793,2795,2797,2799,2802],{"class":35,"line":61},[33,2780,2745],{"class":113},[33,2782,2783],{"class":87},"map",[33,2785,92],{"class":113},[33,2787,2753],{"class":269},[33,2789,2756],{"class":83},[33,2791,2792],{"class":113}," `\u003Cli>${",[33,2794,2753],{"class":91},[33,2796,1124],{"class":113},[33,2798,2764],{"class":91},[33,2800,2801],{"class":113},"}\u003C\u002Fli>`",[33,2803,2804],{"class":113},")\n",[33,2806,2807,2809,2811,2813,2816,2818,2820],{"class":35,"line":67},[33,2808,2745],{"class":113},[33,2810,1419],{"class":87},[33,2812,92],{"class":113},[33,2814,2815],{"class":113},"\"\"",[33,2817,702],{"class":113},[33,2819,954],{"class":113},[33,2821,245],{"class":91},[33,2823,2824],{"class":35,"line":74},[33,2825,132],{"class":91},[2827,2828,2829,2833,2855,2868,2877,3057,3067,3227],"details",{},[2830,2831,2832],"summary",{},"Answer",[2684,2834,2835,2836,2839,2840,2843,2844,2846,2847,2850,2851,1124],{},"The ",[30,2837,2838],{},"item.name"," values are inserted into ",[30,2841,2842],{},"innerHTML"," without escaping. If an attacker can control ",[30,2845,2838],{}," (e.g., by submitting a product name like ",[30,2848,2849],{},"\u003Cimg src=x onerror=\"alert(document.cookie)\">","), the HTML is parsed and the script executes — ",[2852,2853,2854],"strong",{},"stored XSS",[2684,2856,2857,2858,2860,2861,2864,2865,2867],{},"Even though ",[30,2859,2705],{}," is used in ",[30,2862,2863],{},".filter()"," (not in the HTML), the ",[30,2866,2838],{}," values are rendered as raw HTML.",[2684,2869,2870,2873,2874,2876],{},[2852,2871,2872],{},"Fix",": Escape ",[30,2875,2838],{}," before inserting into HTML:",[24,2878,2880],{"className":26,"code":2879,"language":22,"meta":28,"style":28},"function escapeHtml(str) {\n    const div = document.createElement(\"div\");\n    div.textContent = str;\n    return div.innerHTML;\n}\n\nfunction highlightSearch(query) {\n    const results = document.querySelector(\"#results\");\n    results.innerHTML = `Found: ${items\n        .filter(item => item.name.includes(query))\n        .map(item => `\u003Cli>${escapeHtml(item.name)}\u003C\u002Fli>`)  \u002F\u002F ✅ escape each name\n        .join(\"\")}`;\n}\n",[30,2881,2882,2894,2912,2921,2927,2931,2935,2947,2965,2975,3003,3037,3053],{"__ignoreMap":28},[33,2883,2884,2886,2888,2890,2892],{"class":35,"line":36},[33,2885,84],{"class":83},[33,2887,621],{"class":87},[33,2889,92],{"class":91},[33,2891,626],{"class":95},[33,2893,99],{"class":91},[33,2895,2896,2898,2900,2902,2904,2906,2908,2910],{"class":35,"line":43},[33,2897,266],{"class":83},[33,2899,635],{"class":269},[33,2901,273],{"class":83},[33,2903,640],{"class":91},[33,2905,643],{"class":87},[33,2907,92],{"class":91},[33,2909,648],{"class":113},[33,2911,651],{"class":91},[33,2913,2914,2916,2918],{"class":35,"line":49},[33,2915,656],{"class":91},[33,2917,120],{"class":83},[33,2919,2920],{"class":91}," str;\n",[33,2922,2923,2925],{"class":35,"line":55},[33,2924,669],{"class":83},[33,2926,672],{"class":91},[33,2928,2929],{"class":35,"line":61},[33,2930,132],{"class":91},[33,2932,2933],{"class":35,"line":67},[33,2934,71],{"emptyLinePlaceholder":70},[33,2936,2937,2939,2941,2943,2945],{"class":35,"line":74},[33,2938,84],{"class":83},[33,2940,2700],{"class":87},[33,2942,92],{"class":91},[33,2944,2705],{"class":95},[33,2946,99],{"class":91},[33,2948,2949,2951,2953,2955,2957,2959,2961,2963],{"class":35,"line":80},[33,2950,266],{"class":83},[33,2952,2714],{"class":269},[33,2954,273],{"class":83},[33,2956,640],{"class":91},[33,2958,108],{"class":87},[33,2960,92],{"class":91},[33,2962,2725],{"class":113},[33,2964,651],{"class":91},[33,2966,2967,2969,2971,2973],{"class":35,"line":102},[33,2968,2732],{"class":91},[33,2970,120],{"class":83},[33,2972,2737],{"class":113},[33,2974,2740],{"class":91},[33,2976,2977,2979,2981,2983,2985,2987,2989,2991,2993,2995,2997,2999,3001],{"class":35,"line":129},[33,2978,2745],{"class":113},[33,2980,2748],{"class":87},[33,2982,92],{"class":113},[33,2984,2753],{"class":269},[33,2986,2756],{"class":83},[33,2988,2759],{"class":91},[33,2990,1124],{"class":113},[33,2992,2764],{"class":91},[33,2994,1124],{"class":113},[33,2996,2769],{"class":87},[33,2998,92],{"class":113},[33,3000,2705],{"class":91},[33,3002,2776],{"class":113},[33,3004,3005,3007,3009,3011,3013,3015,3017,3019,3021,3023,3025,3027,3029,3031,3034],{"class":35,"line":135},[33,3006,2745],{"class":113},[33,3008,2783],{"class":87},[33,3010,92],{"class":113},[33,3012,2753],{"class":269},[33,3014,2756],{"class":83},[33,3016,2792],{"class":113},[33,3018,695],{"class":87},[33,3020,92],{"class":113},[33,3022,2753],{"class":91},[33,3024,1124],{"class":113},[33,3026,2764],{"class":91},[33,3028,702],{"class":113},[33,3030,2801],{"class":113},[33,3032,3033],{"class":113},")  ",[33,3035,3036],{"class":39},"\u002F\u002F ✅ escape each name\n",[33,3038,3039,3041,3043,3045,3047,3049,3051],{"class":35,"line":152},[33,3040,2745],{"class":113},[33,3042,1419],{"class":87},[33,3044,92],{"class":113},[33,3046,2815],{"class":113},[33,3048,702],{"class":113},[33,3050,954],{"class":113},[33,3052,245],{"class":91},[33,3054,3055],{"class":35,"line":157},[33,3056,132],{"class":91},[2684,3058,3059,3060,3063,3064,3066],{},"Or use ",[30,3061,3062],{},"textContent"," and ",[30,3065,643],{}," (no HTML parsing):",[24,3068,3070],{"className":26,"code":3069,"language":22,"meta":28,"style":28},"function highlightSearch(query) {\n    const results = document.querySelector(\"#results\");\n    results.innerHTML = \"Found:\";\n    const list = document.createElement(\"ul\");\n    for (const item of items.filter(i => i.name.includes(query))) {\n        const li = document.createElement(\"li\");\n        li.textContent = item.name;  \u002F\u002F ✅ textContent (no HTML parsing)\n        list.appendChild(li);\n    }\n    results.appendChild(list);\n}\n",[30,3071,3072,3084,3102,3113,3133,3166,3186,3199,3209,3213,3223],{"__ignoreMap":28},[33,3073,3074,3076,3078,3080,3082],{"class":35,"line":36},[33,3075,84],{"class":83},[33,3077,2700],{"class":87},[33,3079,92],{"class":91},[33,3081,2705],{"class":95},[33,3083,99],{"class":91},[33,3085,3086,3088,3090,3092,3094,3096,3098,3100],{"class":35,"line":43},[33,3087,266],{"class":83},[33,3089,2714],{"class":269},[33,3091,273],{"class":83},[33,3093,640],{"class":91},[33,3095,108],{"class":87},[33,3097,92],{"class":91},[33,3099,2725],{"class":113},[33,3101,651],{"class":91},[33,3103,3104,3106,3108,3111],{"class":35,"line":49},[33,3105,2732],{"class":91},[33,3107,120],{"class":83},[33,3109,3110],{"class":113}," \"Found:\"",[33,3112,245],{"class":91},[33,3114,3115,3117,3120,3122,3124,3126,3128,3131],{"class":35,"line":55},[33,3116,266],{"class":83},[33,3118,3119],{"class":269}," list",[33,3121,273],{"class":83},[33,3123,640],{"class":91},[33,3125,643],{"class":87},[33,3127,92],{"class":91},[33,3129,3130],{"class":113},"\"ul\"",[33,3132,651],{"class":91},[33,3134,3135,3137,3139,3141,3143,3146,3149,3151,3153,3156,3158,3161,3163],{"class":35,"line":61},[33,3136,1547],{"class":83},[33,3138,1550],{"class":91},[33,3140,565],{"class":83},[33,3142,2759],{"class":269},[33,3144,3145],{"class":83}," of",[33,3147,3148],{"class":91}," items.",[33,3150,2748],{"class":87},[33,3152,92],{"class":91},[33,3154,3155],{"class":95},"i",[33,3157,2756],{"class":83},[33,3159,3160],{"class":91}," i.name.",[33,3162,2769],{"class":87},[33,3164,3165],{"class":91},"(query))) {\n",[33,3167,3168,3170,3173,3175,3177,3179,3181,3184],{"class":35,"line":67},[33,3169,792],{"class":83},[33,3171,3172],{"class":269}," li",[33,3174,273],{"class":83},[33,3176,640],{"class":91},[33,3178,643],{"class":87},[33,3180,92],{"class":91},[33,3182,3183],{"class":113},"\"li\"",[33,3185,651],{"class":91},[33,3187,3188,3191,3193,3196],{"class":35,"line":74},[33,3189,3190],{"class":91},"        li.textContent ",[33,3192,120],{"class":83},[33,3194,3195],{"class":91}," item.name;  ",[33,3197,3198],{"class":39},"\u002F\u002F ✅ textContent (no HTML parsing)\n",[33,3200,3201,3204,3206],{"class":35,"line":80},[33,3202,3203],{"class":91},"        list.",[33,3205,2572],{"class":87},[33,3207,3208],{"class":91},"(li);\n",[33,3210,3211],{"class":35,"line":102},[33,3212,1646],{"class":91},[33,3214,3215,3218,3220],{"class":35,"line":129},[33,3216,3217],{"class":91},"    results.",[33,3219,2572],{"class":87},[33,3221,3222],{"class":91},"(list);\n",[33,3224,3225],{"class":35,"line":135},[33,3226,132],{"class":91},[2684,3228,3229,3232,3233,3235,3236,3238,3239,3242],{},[2852,3230,3231],{},"The lesson",": every value inserted into ",[30,3234,2842],{}," must be escaped or sanitized — including data from your own database (stored XSS). Use ",[30,3237,3062],{}," for plain text, or ",[30,3240,3241],{},"DOMPurify.sanitize()"," for rich HTML.",[3244,3245,3246],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .sk71V, html code.shiki .sk71V{--shiki-default:#22863A;--shiki-github-dark:#85E89D}",{"title":28,"searchDepth":43,"depth":43,"links":3248},[3249,3250,3251,3252,3253,3254,3255,3256,3257,3258],{"id":17,"depth":43,"text":18},{"id":547,"depth":43,"text":548},{"id":983,"depth":43,"text":984},{"id":1254,"depth":43,"text":1255},{"id":1496,"depth":43,"text":1497},{"id":1948,"depth":43,"text":1949},{"id":2330,"depth":43,"text":2331},{"id":2414,"depth":43,"text":2415},{"id":2516,"depth":43,"text":2517},{"id":2681,"depth":43,"text":2682},"Deep-dive into JavaScript security: XSS prevention (textContent vs innerHTML, DOMPurify), CSRF tokens and SameSite cookies, Content Security Policy, prototype pollution attacks, and safe input validation. Code-first reference for senior engineers.","md",{},"\u002Fjs\u002F29-security",{"title":5,"description":3259},"js\u002F29-security","ZiBPZyc46qIBZU2xjC_K7Op78Ct5X7Xptll3GEtW_5U",1789924649584]