[{"data":1,"prerenderedAt":1765},["ShallowReactive",2],{"en-post-\u002Fen\u002Fweb-security-sql-injection-xss-csrf":3},{"id":4,"title":5,"body":6,"description":1752,"extension":1753,"meta":1754,"navigation":1025,"path":1760,"seo":1761,"sitemap":1762,"stem":1763,"__hash__":1764},"blogEn\u002Fen\u002Fweb-security-sql-injection-xss-csrf.md","The 3 Basic Web Attacks: How SQL Injection, XSS, and CSRF Work and How to Stop Them",{"type":7,"value":8,"toc":1737},"minimark",[9,14,23,88,91,94,98,103,106,203,210,221,235,238,246,253,259,263,272,290,347,353,404,416,418,422,429,439,1176,1191,1202,1224,1228,1287,1291,1354,1392,1394,1398,1404,1411,1540,1552,1570,1573,1639,1657,1659,1663,1723,1733],[10,11,13],"h2",{"id":12},"what-happens-the-moment-you-trust-input","What happens the moment you trust input",[15,16,17,18,22],"p",{},"Most web security incidents start with one mistake: ",[19,20,21],"strong",{},"using a value the user sent as part of code or a command, as-is",".",[24,25,26,42],"table",{},[27,28,29],"thead",{},[30,31,32,36,39],"tr",{},[33,34,35],"th",{},"Attack",[33,37,38],{},"Where user input becomes \"code\"",[33,40,41],{},"Damage",[43,44,45,60,74],"tbody",{},[30,46,47,51,57],{},[48,49,50],"td",{},"SQL Injection",[48,52,53,54],{},"Inside the server's ",[19,55,56],{},"SQL query",[48,58,59],{},"Login bypass, data theft or deletion",[30,61,62,65,71],{},[48,63,64],{},"XSS",[48,66,67,68],{},"Inside ",[19,69,70],{},"another user's browser HTML",[48,72,73],{},"Session theft, acting as the user",[30,75,76,79,85],{},[48,77,78],{},"CSRF",[48,80,81,82],{},"(Not input) abuses ",[19,83,84],{},"cookies the browser sends automatically",[48,86,87],{},"Transfers or password changes without the user knowing",[15,89,90],{},"These three are classic attacks that keep appearing on web security risk lists like the OWASP Top 10. Let's attack and defend each one.",[92,93],"hr",{},[10,95,97],{"id":96},"_1-sql-injection-when-input-becomes-the-query","1. SQL Injection: when input becomes the query",[99,100,102],"h3",{"id":101},"try-the-attack","Try the attack",[15,104,105],{},"Say login is implemented like this: the email and password the user typed are concatenated into the SQL string.",[107,108,113],"pre",{"className":109,"code":110,"language":111,"meta":112,"style":112},"language-js shiki shiki-themes github-light github-dark","function login(email, password) {\n  const sql = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`;\n  return db.prepare(sql).get();\n}\n","js","",[114,115,116,146,176,197],"code",{"__ignoreMap":112},[117,118,121,125,129,133,137,140,143],"span",{"class":119,"line":120},"line",1,[117,122,124],{"class":123},"szBVR","function",[117,126,128],{"class":127},"sScJk"," login",[117,130,132],{"class":131},"sVt8B","(",[117,134,136],{"class":135},"s4XuR","email",[117,138,139],{"class":131},", ",[117,141,142],{"class":135},"password",[117,144,145],{"class":131},") {\n",[117,147,149,152,156,159,163,165,168,170,173],{"class":119,"line":148},2,[117,150,151],{"class":123},"  const",[117,153,155],{"class":154},"sj4cs"," sql",[117,157,158],{"class":123}," =",[117,160,162],{"class":161},"sZZnC"," `SELECT * FROM users WHERE email = '${",[117,164,136],{"class":131},[117,166,167],{"class":161},"}' AND password = '${",[117,169,142],{"class":131},[117,171,172],{"class":161},"}'`",[117,174,175],{"class":131},";\n",[117,177,179,182,185,188,191,194],{"class":119,"line":178},3,[117,180,181],{"class":123},"  return",[117,183,184],{"class":131}," db.",[117,186,187],{"class":127},"prepare",[117,189,190],{"class":131},"(sql).",[117,192,193],{"class":127},"get",[117,195,196],{"class":131},"();\n",[117,198,200],{"class":119,"line":199},4,[117,201,202],{"class":131},"}\n",[15,204,205,206,209],{},"Normal input works fine. But type ",[114,207,208],{},"admin@site.com' --"," into the email field and the SQL becomes:",[107,211,215],{"className":212,"code":213,"language":214,"meta":112,"style":112},"language-sql shiki shiki-themes github-light github-dark","SELECT * FROM users WHERE email = 'admin@site.com' --' AND password = 'anything'\n","sql",[114,216,217],{"__ignoreMap":112},[117,218,219],{"class":119,"line":120},[117,220,213],{},[15,222,223,226,227,230,231,234],{},[114,224,225],{},"--"," starts a SQL comment. ",[19,228,229],{},"The whole password check is commented out",", and you're logged in as admin without a password. If you don't even know the email, ",[114,232,233],{},"' OR '1'='1' --"," makes the condition always true and logs you in as the first user (usually the admin).",[15,236,237],{},"Running it on SQLite, both attacks return the admin account:",[107,239,244],{"className":240,"code":242,"language":243},[241],"language-text","attack 1: { id: 1, email: 'admin@site.com', role: 'admin' }\nattack 2: { id: 1, email: 'admin@site.com', role: 'admin' }\n","text",[114,245,242],{"__ignoreMap":112},[15,247,248,249,252],{},"The same mistake in a search feature is even worse: ",[114,250,251],{},"UNION"," lets you pull data from other tables.",[107,254,257],{"className":255,"code":256,"language":243},[241],"search:  zzz' UNION SELECT email || ':' || password FROM users --\nresult:  admin@site.com:s3cret!, kim@site.com:hunter2   ← every account's password\n",[114,258,256],{"__ignoreMap":112},[99,260,262],{"id":261},"defense-parameter-binding","Defense: parameter binding",[264,265,266],"blockquote",{},[15,267,268,269],{},"There's one way to prevent SQL Injection: ",[19,270,271],{},"never concatenate input into the SQL string.",[15,273,274,275,278,279,282,283,286,287,289],{},"Put a ",[114,276,277],{},"?"," (placeholder) where the value goes and pass the value separately. The database parses the SQL structure first and ",[19,280,281],{},"always treats the value as just a value",". A ",[114,284,285],{},"'"," or ",[114,288,225],{}," is just a character.",[107,291,293],{"className":109,"code":292,"language":111,"meta":112,"style":112},"function login(email, password) {\n  return db\n    .prepare(\"SELECT * FROM users WHERE email = ? AND password = ?\")\n    .get(email, password);\n}\n",[114,294,295,311,318,333,342],{"__ignoreMap":112},[117,296,297,299,301,303,305,307,309],{"class":119,"line":120},[117,298,124],{"class":123},[117,300,128],{"class":127},[117,302,132],{"class":131},[117,304,136],{"class":135},[117,306,139],{"class":131},[117,308,142],{"class":135},[117,310,145],{"class":131},[117,312,313,315],{"class":119,"line":148},[117,314,181],{"class":123},[117,316,317],{"class":131}," db\n",[117,319,320,323,325,327,330],{"class":119,"line":178},[117,321,322],{"class":131},"    .",[117,324,187],{"class":127},[117,326,132],{"class":131},[117,328,329],{"class":161},"\"SELECT * FROM users WHERE email = ? AND password = ?\"",[117,331,332],{"class":131},")\n",[117,334,335,337,339],{"class":119,"line":199},[117,336,322],{"class":131},[117,338,193],{"class":127},[117,340,341],{"class":131},"(email, password);\n",[117,343,345],{"class":119,"line":344},5,[117,346,202],{"class":131},[107,348,351],{"className":349,"code":350,"language":243},[241],"attack 1: undefined   ← login fails\nattack 2: undefined   ← login fails\nnormal login: { id: 2, email: 'kim@site.com', role: 'user' }\n",[114,352,350],{"__ignoreMap":112},[24,354,355,365],{},[27,356,357],{},[30,358,359,362],{},[33,360,361],{},"Tool",[33,363,364],{},"The safe way",[43,366,367,381,392],{},[30,368,369,372],{},[48,370,371],{},"Node.js DB drivers",[48,373,374,375,286,378],{},"Placeholders like ",[114,376,377],{},"query(\"... WHERE id = ?\", [id])",[114,379,380],{},"$1",[30,382,383,386],{},[48,384,385],{},"ORMs (Prisma, Sequelize, etc.)",[48,387,388,389],{},"The standard API is safe. Watch out ",[19,390,391],{},"when concatenating strings into raw queries",[30,393,394,397],{},[48,395,396],{},"Places placeholders can't go (table names, sort columns)",[48,398,399,400,403],{},"Only use values from an allowlist (",[114,401,402],{},"[\"name\", \"date\"]",")",[405,406,407],"warning",{},[264,408,409],{},[15,410,411,412,415],{},"Storing passwords ",[19,413,414],{},"in plain text",", as in the example above, is a serious problem on its own. Even without SQL Injection, the moment the DB leaks, every password is exposed. Store passwords with a dedicated hash function like bcrypt or argon2.",[92,417],{},[10,419,421],{"id":420},"_2-xss-when-input-runs-in-someone-elses-browser","2. XSS: when input runs in someone else's browser",[15,423,424,425,428],{},"XSS (Cross-Site Scripting) is when content an attacker submitted ",[19,426,427],{},"gets interpreted as HTML on other users' pages and runs scripts",". Anywhere you display user input back on screen, like comments, nicknames, and search terms, is a target.",[15,430,431,432,435,436,22],{},"Try it in the preview below. It outputs the same comment with ",[114,433,434],{},"innerHTML"," and with ",[114,437,438],{},"textContent",[440,441,442,609,965],"code-group",{},[107,443,447],{"className":444,"code":445,"language":446,"meta":112,"style":112},"language-html shiki shiki-themes github-light github-dark","\u003Clabel for=\"comment\">Comment\u003C\u002Flabel>\n\u003Cinput id=\"comment\" \u002F>\n\u003Cdiv class=\"btns\">\n  \u003Cbutton id=\"unsafe\">Output with innerHTML (unsafe)\u003C\u002Fbutton>\n  \u003Cbutton id=\"safe\">Output with textContent (safe)\u003C\u002Fbutton>\n\u003C\u002Fdiv>\n\u003Cdiv class=\"out\" id=\"out\">The comment will appear here\u003C\u002Fdiv>\n\u003Cp id=\"alarm\">\u003C\u002Fp>\n","html",[114,448,449,475,492,509,531,551,561,588],{"__ignoreMap":112},[117,450,451,454,458,461,464,467,470,472],{"class":119,"line":120},[117,452,453],{"class":131},"\u003C",[117,455,457],{"class":456},"s9eBZ","label",[117,459,460],{"class":127}," for",[117,462,463],{"class":131},"=",[117,465,466],{"class":161},"\"comment\"",[117,468,469],{"class":131},">Comment\u003C\u002F",[117,471,457],{"class":456},[117,473,474],{"class":131},">\n",[117,476,477,479,482,485,487,489],{"class":119,"line":148},[117,478,453],{"class":131},[117,480,481],{"class":456},"input",[117,483,484],{"class":127}," id",[117,486,463],{"class":131},[117,488,466],{"class":161},[117,490,491],{"class":131}," \u002F>\n",[117,493,494,496,499,502,504,507],{"class":119,"line":178},[117,495,453],{"class":131},[117,497,498],{"class":456},"div",[117,500,501],{"class":127}," class",[117,503,463],{"class":131},[117,505,506],{"class":161},"\"btns\"",[117,508,474],{"class":131},[117,510,511,514,517,519,521,524,527,529],{"class":119,"line":199},[117,512,513],{"class":131},"  \u003C",[117,515,516],{"class":456},"button",[117,518,484],{"class":127},[117,520,463],{"class":131},[117,522,523],{"class":161},"\"unsafe\"",[117,525,526],{"class":131},">Output with innerHTML (unsafe)\u003C\u002F",[117,528,516],{"class":456},[117,530,474],{"class":131},[117,532,533,535,537,539,541,544,547,549],{"class":119,"line":344},[117,534,513],{"class":131},[117,536,516],{"class":456},[117,538,484],{"class":127},[117,540,463],{"class":131},[117,542,543],{"class":161},"\"safe\"",[117,545,546],{"class":131},">Output with textContent (safe)\u003C\u002F",[117,548,516],{"class":456},[117,550,474],{"class":131},[117,552,554,557,559],{"class":119,"line":553},6,[117,555,556],{"class":131},"\u003C\u002F",[117,558,498],{"class":456},[117,560,474],{"class":131},[117,562,564,566,568,570,572,575,577,579,581,584,586],{"class":119,"line":563},7,[117,565,453],{"class":131},[117,567,498],{"class":456},[117,569,501],{"class":127},[117,571,463],{"class":131},[117,573,574],{"class":161},"\"out\"",[117,576,484],{"class":127},[117,578,463],{"class":131},[117,580,574],{"class":161},[117,582,583],{"class":131},">The comment will appear here\u003C\u002F",[117,585,498],{"class":456},[117,587,474],{"class":131},[117,589,591,593,595,597,599,602,605,607],{"class":119,"line":590},8,[117,592,453],{"class":131},[117,594,15],{"class":456},[117,596,484],{"class":127},[117,598,463],{"class":131},[117,600,601],{"class":161},"\"alarm\"",[117,603,604],{"class":131},">\u003C\u002F",[117,606,15],{"class":456},[117,608,474],{"class":131},[107,610,614],{"className":611,"code":612,"language":613,"meta":112,"style":112},"language-css shiki shiki-themes github-light github-dark","label { font-weight: 600; }\ninput { width: 100%; box-sizing: border-box; font: 13px ui-monospace, monospace; padding: 6px 8px; margin: 6px 0; }\n.btns { display: flex; gap: 6px; flex-wrap: wrap; }\nbutton { font: inherit; font-size: 14px; padding: 6px 10px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer; }\n.out { margin-top: 12px; padding: 12px; border: 1px dashed #bbb; border-radius: 8px; min-height: 24px; word-break: break-all; }\n#alarm { font-weight: 700; color: #dc2626; margin: 10px 0 0; }\n","css",[114,615,616,635,717,755,847,924],{"__ignoreMap":112},[117,617,618,620,623,626,629,632],{"class":119,"line":120},[117,619,457],{"class":456},[117,621,622],{"class":131}," { ",[117,624,625],{"class":154},"font-weight",[117,627,628],{"class":131},": ",[117,630,631],{"class":154},"600",[117,633,634],{"class":131},"; }\n",[117,636,637,639,641,644,646,649,652,655,658,660,663,665,668,670,673,676,679,681,684,686,689,691,694,696,699,701,703,706,708,710,712,715],{"class":119,"line":148},[117,638,481],{"class":456},[117,640,622],{"class":131},[117,642,643],{"class":154},"width",[117,645,628],{"class":131},[117,647,648],{"class":154},"100",[117,650,651],{"class":123},"%",[117,653,654],{"class":131},"; ",[117,656,657],{"class":154},"box-sizing",[117,659,628],{"class":131},[117,661,662],{"class":154},"border-box",[117,664,654],{"class":131},[117,666,667],{"class":154},"font",[117,669,628],{"class":131},[117,671,672],{"class":154},"13",[117,674,675],{"class":123},"px",[117,677,678],{"class":154}," ui-monospace",[117,680,139],{"class":131},[117,682,683],{"class":154},"monospace",[117,685,654],{"class":131},[117,687,688],{"class":154},"padding",[117,690,628],{"class":131},[117,692,693],{"class":154},"6",[117,695,675],{"class":123},[117,697,698],{"class":154}," 8",[117,700,675],{"class":123},[117,702,654],{"class":131},[117,704,705],{"class":154},"margin",[117,707,628],{"class":131},[117,709,693],{"class":154},[117,711,675],{"class":123},[117,713,714],{"class":154}," 0",[117,716,634],{"class":131},[117,718,719,722,724,727,729,732,734,737,739,741,743,745,748,750,753],{"class":119,"line":178},[117,720,721],{"class":127},".btns",[117,723,622],{"class":131},[117,725,726],{"class":154},"display",[117,728,628],{"class":131},[117,730,731],{"class":154},"flex",[117,733,654],{"class":131},[117,735,736],{"class":154},"gap",[117,738,628],{"class":131},[117,740,693],{"class":154},[117,742,675],{"class":123},[117,744,654],{"class":131},[117,746,747],{"class":154},"flex-wrap",[117,749,628],{"class":131},[117,751,752],{"class":154},"wrap",[117,754,634],{"class":131},[117,756,757,759,761,763,765,768,770,773,775,778,780,782,784,786,788,790,793,795,797,800,802,805,807,810,813,815,818,820,823,825,827,830,832,835,837,840,842,845],{"class":119,"line":199},[117,758,516],{"class":456},[117,760,622],{"class":131},[117,762,667],{"class":154},[117,764,628],{"class":131},[117,766,767],{"class":154},"inherit",[117,769,654],{"class":131},[117,771,772],{"class":154},"font-size",[117,774,628],{"class":131},[117,776,777],{"class":154},"14",[117,779,675],{"class":123},[117,781,654],{"class":131},[117,783,688],{"class":154},[117,785,628],{"class":131},[117,787,693],{"class":154},[117,789,675],{"class":123},[117,791,792],{"class":154}," 10",[117,794,675],{"class":123},[117,796,654],{"class":131},[117,798,799],{"class":154},"border",[117,801,628],{"class":131},[117,803,804],{"class":154},"1",[117,806,675],{"class":123},[117,808,809],{"class":154}," solid",[117,811,812],{"class":154}," #ccc",[117,814,654],{"class":131},[117,816,817],{"class":154},"border-radius",[117,819,628],{"class":131},[117,821,822],{"class":154},"8",[117,824,675],{"class":123},[117,826,654],{"class":131},[117,828,829],{"class":154},"background",[117,831,628],{"class":131},[117,833,834],{"class":154},"#fff",[117,836,654],{"class":131},[117,838,839],{"class":154},"cursor",[117,841,628],{"class":131},[117,843,844],{"class":154},"pointer",[117,846,634],{"class":131},[117,848,849,852,854,857,859,862,864,866,868,870,872,874,876,878,880,882,884,887,890,892,894,896,898,900,902,905,907,910,912,914,917,919,922],{"class":119,"line":344},[117,850,851],{"class":127},".out",[117,853,622],{"class":131},[117,855,856],{"class":154},"margin-top",[117,858,628],{"class":131},[117,860,861],{"class":154},"12",[117,863,675],{"class":123},[117,865,654],{"class":131},[117,867,688],{"class":154},[117,869,628],{"class":131},[117,871,861],{"class":154},[117,873,675],{"class":123},[117,875,654],{"class":131},[117,877,799],{"class":154},[117,879,628],{"class":131},[117,881,804],{"class":154},[117,883,675],{"class":123},[117,885,886],{"class":154}," dashed",[117,888,889],{"class":154}," #bbb",[117,891,654],{"class":131},[117,893,817],{"class":154},[117,895,628],{"class":131},[117,897,822],{"class":154},[117,899,675],{"class":123},[117,901,654],{"class":131},[117,903,904],{"class":154},"min-height",[117,906,628],{"class":131},[117,908,909],{"class":154},"24",[117,911,675],{"class":123},[117,913,654],{"class":131},[117,915,916],{"class":154},"word-break",[117,918,628],{"class":131},[117,920,921],{"class":154},"break-all",[117,923,634],{"class":131},[117,925,926,929,931,933,935,938,940,943,945,948,950,952,954,957,959,961,963],{"class":119,"line":553},[117,927,928],{"class":127},"#alarm",[117,930,622],{"class":131},[117,932,625],{"class":154},[117,934,628],{"class":131},[117,936,937],{"class":154},"700",[117,939,654],{"class":131},[117,941,942],{"class":154},"color",[117,944,628],{"class":131},[117,946,947],{"class":154},"#dc2626",[117,949,654],{"class":131},[117,951,705],{"class":154},[117,953,628],{"class":131},[117,955,956],{"class":154},"10",[117,958,675],{"class":123},[117,960,714],{"class":154},[117,962,714],{"class":154},[117,964,634],{"class":131},[107,966,968],{"className":109,"code":967,"language":111,"meta":112,"style":112},"const $ = (s) => document.querySelector(s);\n$(\"#comment\").value = `Great post \u003Cimg src=x onerror=\"document.getElementById('alarm').textContent='⚠️ Script ran! It could steal cookies or send requests as the user here'\">`;\n\n$(\"#unsafe\").addEventListener(\"click\", () => {\n  $(\"#alarm\").textContent = \"\";\n  $(\"#out\").innerHTML = $(\"#comment\").value; \u002F\u002F input parsed as HTML\n});\n$(\"#safe\").addEventListener(\"click\", () => {\n  $(\"#alarm\").textContent = \"\";\n  $(\"#out\").textContent = $(\"#comment\").value; \u002F\u002F input shown as plain text\n});\n",[114,969,970,1001,1021,1027,1055,1075,1102,1107,1130,1147,1171],{"__ignoreMap":112},[117,971,972,975,978,980,983,986,989,992,995,998],{"class":119,"line":120},[117,973,974],{"class":123},"const",[117,976,977],{"class":127}," $",[117,979,158],{"class":123},[117,981,982],{"class":131}," (",[117,984,985],{"class":135},"s",[117,987,988],{"class":131},") ",[117,990,991],{"class":123},"=>",[117,993,994],{"class":131}," document.",[117,996,997],{"class":127},"querySelector",[117,999,1000],{"class":131},"(s);\n",[117,1002,1003,1006,1008,1011,1014,1016,1019],{"class":119,"line":148},[117,1004,1005],{"class":127},"$",[117,1007,132],{"class":131},[117,1009,1010],{"class":161},"\"#comment\"",[117,1012,1013],{"class":131},").value ",[117,1015,463],{"class":123},[117,1017,1018],{"class":161}," `Great post \u003Cimg src=x onerror=\"document.getElementById('alarm').textContent='⚠️ Script ran! It could steal cookies or send requests as the user here'\">`",[117,1020,175],{"class":131},[117,1022,1023],{"class":119,"line":178},[117,1024,1026],{"emptyLinePlaceholder":1025},true,"\n",[117,1028,1029,1031,1033,1036,1039,1042,1044,1047,1050,1052],{"class":119,"line":199},[117,1030,1005],{"class":127},[117,1032,132],{"class":131},[117,1034,1035],{"class":161},"\"#unsafe\"",[117,1037,1038],{"class":131},").",[117,1040,1041],{"class":127},"addEventListener",[117,1043,132],{"class":131},[117,1045,1046],{"class":161},"\"click\"",[117,1048,1049],{"class":131},", () ",[117,1051,991],{"class":123},[117,1053,1054],{"class":131}," {\n",[117,1056,1057,1060,1062,1065,1068,1070,1073],{"class":119,"line":344},[117,1058,1059],{"class":127},"  $",[117,1061,132],{"class":131},[117,1063,1064],{"class":161},"\"#alarm\"",[117,1066,1067],{"class":131},").textContent ",[117,1069,463],{"class":123},[117,1071,1072],{"class":161}," \"\"",[117,1074,175],{"class":131},[117,1076,1077,1079,1081,1084,1087,1089,1091,1093,1095,1098],{"class":119,"line":553},[117,1078,1059],{"class":127},[117,1080,132],{"class":131},[117,1082,1083],{"class":161},"\"#out\"",[117,1085,1086],{"class":131},").innerHTML ",[117,1088,463],{"class":123},[117,1090,977],{"class":127},[117,1092,132],{"class":131},[117,1094,1010],{"class":161},[117,1096,1097],{"class":131},").value; ",[117,1099,1101],{"class":1100},"sJ8bj","\u002F\u002F input parsed as HTML\n",[117,1103,1104],{"class":119,"line":563},[117,1105,1106],{"class":131},"});\n",[117,1108,1109,1111,1113,1116,1118,1120,1122,1124,1126,1128],{"class":119,"line":590},[117,1110,1005],{"class":127},[117,1112,132],{"class":131},[117,1114,1115],{"class":161},"\"#safe\"",[117,1117,1038],{"class":131},[117,1119,1041],{"class":127},[117,1121,132],{"class":131},[117,1123,1046],{"class":161},[117,1125,1049],{"class":131},[117,1127,991],{"class":123},[117,1129,1054],{"class":131},[117,1131,1133,1135,1137,1139,1141,1143,1145],{"class":119,"line":1132},9,[117,1134,1059],{"class":127},[117,1136,132],{"class":131},[117,1138,1064],{"class":161},[117,1140,1067],{"class":131},[117,1142,463],{"class":123},[117,1144,1072],{"class":161},[117,1146,175],{"class":131},[117,1148,1150,1152,1154,1156,1158,1160,1162,1164,1166,1168],{"class":119,"line":1149},10,[117,1151,1059],{"class":127},[117,1153,132],{"class":131},[117,1155,1083],{"class":161},[117,1157,1067],{"class":131},[117,1159,463],{"class":123},[117,1161,977],{"class":127},[117,1163,132],{"class":131},[117,1165,1010],{"class":161},[117,1167,1097],{"class":131},[117,1169,1170],{"class":1100},"\u002F\u002F input shown as plain text\n",[117,1172,1174],{"class":119,"line":1173},11,[117,1175,1106],{"class":131},[15,1177,1178,1179,1182,1183,1186,1187,1190],{},"Press \"Output with innerHTML\" and the ",[114,1180,1181],{},"\u003Cimg>"," tag inside the comment is parsed as a real image tag. When the image fails to load, the code in its ",[114,1184,1185],{},"onerror"," attribute runs ",[19,1188,1189],{},"in the browser of whoever views the comment",". In a real attack, this is where it would send cookies to the attacker's server, or post content and change passwords on the user's behalf.",[15,1192,1193,1194,1197,1198,1201],{},"\"Output with textContent\" shows the same input ",[19,1195,1196],{},"as literal text",". ",[114,1199,1200],{},"\u003Cimg ...>"," just appears as characters on screen, and nothing runs.",[1203,1204,1205],"note",{},[264,1206,1207],{},[15,1208,1209,1210,1213,1214,1216,1217,1219,1220,1223],{},"Put ",[114,1211,1212],{},"\u003Cscript>alert(1)\u003C\u002Fscript>"," into ",[114,1215,434],{}," and it won't run. That makes it easy to think you're \"protected,\" but event attributes like ",[114,1218,1185],{}," and ",[114,1221,1222],{},"onload"," still run. That's why filtering out specific tags can't stop XSS.",[99,1225,1227],{"id":1226},"kinds-of-xss","Kinds of XSS",[24,1229,1230,1243],{},[27,1231,1232],{},[30,1233,1234,1237,1240],{},[33,1235,1236],{},"Kind",[33,1238,1239],{},"Where the malicious input comes from",[33,1241,1242],{},"Example",[43,1244,1245,1256,1271],{},[30,1246,1247,1250,1253],{},[48,1248,1249],{},"Stored",[48,1251,1252],{},"Saved in the DB and shown to many people",[48,1254,1255],{},"Posts, comments, profiles",[30,1257,1258,1261,1264],{},[48,1259,1260],{},"Reflected",[48,1262,1263],{},"A URL parameter is echoed into the response",[48,1265,1266,1267,1270],{},"Luring someone to click a ",[114,1268,1269],{},"?q=\u003Cimg onerror=...>"," link",[30,1272,1273,1276,1279],{},[48,1274,1275],{},"DOM-based",[48,1277,1278],{},"Frontend code inserts it directly, without the server",[48,1280,1281,1282,1213,1285],{},"Putting ",[114,1283,1284],{},"location.hash",[114,1286,434],{},[99,1288,1290],{"id":1289},"defense","Defense",[1292,1293,1294,1318,1338,1344],"ol",{},[1295,1296,1297,1300,1301,139,1303,139,1306,139,1309,1311,1312,1315,1316,22],"li",{},[19,1298,1299],{},"Escape on output."," Turn ",[114,1302,453],{},[114,1304,1305],{},">",[114,1307,1308],{},"\"",[114,1310,285],{},", and ",[114,1313,1314],{},"&"," into HTML entities so they render as text. Insert text with ",[114,1317,438],{},[1295,1319,1320,1323,1324,1327,1328,1331,1332,1327,1335,22],{},[19,1321,1322],{},"Use your framework's default output."," Vue's ",[114,1325,1326],{},"{{ }}"," and React's ",[114,1329,1330],{},"{ }"," escape automatically. The dangerous parts are the escape hatches: Vue's ",[114,1333,1334],{},"v-html",[114,1336,1337],{},"dangerouslySetInnerHTML",[1295,1339,1340,1343],{},[19,1341,1342],{},"If you must allow HTML, sanitize it."," For something like a rich-text editor, keep only allowed tags with a proven library such as DOMPurify.",[1295,1345,1346,1349,1350,1353],{},[19,1347,1348],{},"Add damage limiters."," An ",[114,1351,1352],{},"HttpOnly"," session cookie can't be read by scripts. A CSP (Content-Security-Policy) header can block scripts that aren't allowed.",[107,1355,1357],{"className":109,"code":1356,"language":111,"meta":112,"style":112},"\u002F\u002F ❌ User input as HTML\nel.innerHTML = comment;\n\n\u002F\u002F ✅ As text\nel.textContent = comment;\n",[114,1358,1359,1364,1374,1378,1383],{"__ignoreMap":112},[117,1360,1361],{"class":119,"line":120},[117,1362,1363],{"class":1100},"\u002F\u002F ❌ User input as HTML\n",[117,1365,1366,1369,1371],{"class":119,"line":148},[117,1367,1368],{"class":131},"el.innerHTML ",[117,1370,463],{"class":123},[117,1372,1373],{"class":131}," comment;\n",[117,1375,1376],{"class":119,"line":178},[117,1377,1026],{"emptyLinePlaceholder":1025},[117,1379,1380],{"class":119,"line":199},[117,1381,1382],{"class":1100},"\u002F\u002F ✅ As text\n",[117,1384,1385,1388,1390],{"class":119,"line":344},[117,1386,1387],{"class":131},"el.textContent ",[117,1389,463],{"class":123},[117,1391,1373],{"class":131},[92,1393],{},[10,1395,1397],{"id":1396},"_3-csrf-abusing-cookies-the-browser-sends-automatically","3. CSRF: abusing cookies the browser sends automatically",[15,1399,1400,1401,22],{},"CSRF (Cross-Site Request Forgery) is different from the first two. It doesn't inject code. Instead it exploits the fact that ",[19,1402,1403],{},"the browser automatically attaches cookies to every request",[15,1405,1406,1407,1410],{},"Say a user is logged into a bank site (",[114,1408,1409],{},"bank.com",") and visits a page the attacker made. That page hides a form like this:",[107,1412,1414],{"className":444,"code":1413,"language":446,"meta":112,"style":112},"\u003C!-- A page on evil.com -->\n\u003Cform action=\"https:\u002F\u002Fbank.com\u002Ftransfer\" method=\"POST\">\n  \u003Cinput type=\"hidden\" name=\"to\" value=\"attacker\" \u002F>\n  \u003Cinput type=\"hidden\" name=\"amount\" value=\"1000000\" \u002F>\n\u003C\u002Fform>\n\u003Cscript>document.forms[0].submit();\u003C\u002Fscript>\n",[114,1415,1416,1421,1446,1478,1506,1514],{"__ignoreMap":112},[117,1417,1418],{"class":119,"line":120},[117,1419,1420],{"class":1100},"\u003C!-- A page on evil.com -->\n",[117,1422,1423,1425,1428,1431,1433,1436,1439,1441,1444],{"class":119,"line":148},[117,1424,453],{"class":131},[117,1426,1427],{"class":456},"form",[117,1429,1430],{"class":127}," action",[117,1432,463],{"class":131},[117,1434,1435],{"class":161},"\"https:\u002F\u002Fbank.com\u002Ftransfer\"",[117,1437,1438],{"class":127}," method",[117,1440,463],{"class":131},[117,1442,1443],{"class":161},"\"POST\"",[117,1445,474],{"class":131},[117,1447,1448,1450,1452,1455,1457,1460,1463,1465,1468,1471,1473,1476],{"class":119,"line":178},[117,1449,513],{"class":131},[117,1451,481],{"class":456},[117,1453,1454],{"class":127}," type",[117,1456,463],{"class":131},[117,1458,1459],{"class":161},"\"hidden\"",[117,1461,1462],{"class":127}," name",[117,1464,463],{"class":131},[117,1466,1467],{"class":161},"\"to\"",[117,1469,1470],{"class":127}," value",[117,1472,463],{"class":131},[117,1474,1475],{"class":161},"\"attacker\"",[117,1477,491],{"class":131},[117,1479,1480,1482,1484,1486,1488,1490,1492,1494,1497,1499,1501,1504],{"class":119,"line":199},[117,1481,513],{"class":131},[117,1483,481],{"class":456},[117,1485,1454],{"class":127},[117,1487,463],{"class":131},[117,1489,1459],{"class":161},[117,1491,1462],{"class":127},[117,1493,463],{"class":131},[117,1495,1496],{"class":161},"\"amount\"",[117,1498,1470],{"class":127},[117,1500,463],{"class":131},[117,1502,1503],{"class":161},"\"1000000\"",[117,1505,491],{"class":131},[117,1507,1508,1510,1512],{"class":119,"line":344},[117,1509,556],{"class":131},[117,1511,1427],{"class":456},[117,1513,474],{"class":131},[117,1515,1516,1518,1521,1524,1527,1530,1533,1536,1538],{"class":119,"line":553},[117,1517,453],{"class":131},[117,1519,1520],{"class":456},"script",[117,1522,1523],{"class":131},">document.forms[",[117,1525,1526],{"class":154},"0",[117,1528,1529],{"class":131},"].",[117,1531,1532],{"class":127},"submit",[117,1534,1535],{"class":131},"();\u003C\u002F",[117,1537,1520],{"class":456},[117,1539,474],{"class":131},[15,1541,1542,1543,1545,1546,1548,1549,22],{},"The moment the page opens, a transfer request goes to ",[114,1544,1409],{},". The browser attaches ",[114,1547,1409],{},"'s login cookie, and the bank server can't tell it apart from ",[19,1550,1551],{},"a legitimate request the logged-in user sent themselves",[1553,1554,1555],"tip",{},[264,1556,1557],{},[15,1558,1559,1560,1565,1566,1569],{},"As the ",[1561,1562,1564],"a",{"href":1563},"\u002Fen\u002Fcors-guide","CORS post"," showed, simple requests like form submissions aren't blocked by CORS and do reach the server. CORS only stops the response from being read, so it can't help when ",[19,1567,1568],{},"the request arriving is itself the damage",", like a money transfer.",[99,1571,1290],{"id":1572},"defense-1",[24,1574,1575,1585],{},[27,1576,1577],{},[30,1578,1579,1582],{},[33,1580,1581],{},"Method",[33,1583,1584],{},"How it works",[43,1586,1587,1601,1609,1621],{},[30,1588,1589,1598],{},[48,1590,1591,286,1594,1597],{},[114,1592,1593],{},"SameSite=Lax",[114,1595,1596],{},"Strict"," on cookies",[48,1599,1600],{},"Cookies aren't attached to requests started from other sites. Stops most CSRF",[30,1602,1603,1606],{},[48,1604,1605],{},"CSRF token",[48,1607,1608],{},"The server embeds an unpredictable token in each form and checks for it on submit. The attacker's page can't know it",[30,1610,1611,1618],{},[48,1612,1613,1614,1617],{},"Check the ",[114,1615,1616],{},"Origin"," header",[48,1619,1620],{},"Verify that state-changing requests come from your own site",[30,1622,1623,1629],{},[48,1624,1625,1626],{},"Never change state with ",[114,1627,1628],{},"GET",[48,1630,1631,1632,1635,1636],{},"A URL like ",[114,1633,1634],{},"GET \u002Fdelete?id=3"," can be triggered by a single ",[114,1637,1638],{},"\u003Cimg src>",[15,1640,1641,1642,1645,1646,1649,1650,1652,1653,22],{},"Modern browsers may treat cookies without a ",[114,1643,1644],{},"SameSite"," attribute like ",[114,1647,1648],{},"Lax",", but behavior isn't identical across browsers. Setting ",[114,1651,1644],{}," explicitly on auth cookies is the safe choice. Cookie attributes are covered in more detail in the ",[1561,1654,1656],{"href":1655},"\u002Fen\u002Fjwt-vs-session-auth","JWT vs sessions post",[92,1658],{},[10,1660,1662],{"id":1661},"summary-why-this-is-worth-knowing","Summary: why this is worth knowing",[24,1664,1665,1677],{},[27,1666,1667],{},[30,1668,1669,1671,1674],{},[33,1670,35],{},[33,1672,1673],{},"Root cause",[33,1675,1676],{},"Core defense",[43,1678,1679,1692,1708],{},[30,1680,1681,1683,1686],{},[48,1682,50],{},[48,1684,1685],{},"Concatenating input into SQL strings",[48,1687,1688,1689,1691],{},"Parameter binding (",[114,1690,277],{}," placeholders)",[30,1693,1694,1696,1699],{},[48,1695,64],{},[48,1697,1698],{},"Parsing input as HTML on output",[48,1700,1701,1702,1704,1705,1707],{},"Escape on output (",[114,1703,438],{},", framework default output), careful with ",[114,1706,1334],{}," etc., CSP",[30,1709,1710,1712,1715],{},[48,1711,78],{},[48,1713,1714],{},"Browsers send cookies automatically",[48,1716,1717,1719,1720,1722],{},[114,1718,1644],{}," cookies, CSRF tokens, ",[114,1721,1616],{}," checks",[15,1724,1725,1726,1729,1730],{},"All three happen ",[19,1727,1728],{},"when the line between \"data\" and \"code\" breaks down",". In SQL, a value becomes query structure; in HTML, text becomes a tag; in CSRF, the attacker's request becomes the user's request. The defense follows the same principle: ",[19,1731,1732],{},"treat user input as data all the way through, and confirm that a request really is what the user intended.",[1734,1735,1736],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":112,"searchDepth":148,"depth":148,"links":1738},[1739,1740,1744,1748,1751],{"id":12,"depth":148,"text":13},{"id":96,"depth":148,"text":97,"children":1741},[1742,1743],{"id":101,"depth":178,"text":102},{"id":261,"depth":178,"text":262},{"id":420,"depth":148,"text":421,"children":1745},[1746,1747],{"id":1226,"depth":178,"text":1227},{"id":1289,"depth":178,"text":1290},{"id":1396,"depth":148,"text":1397,"children":1749},[1750],{"id":1572,"depth":178,"text":1290},{"id":1661,"depth":148,"text":1662},"One line, ' OR '1'='1, in a login form logs you in as admin, and a single comment can run scripts in other users' browsers. How SQL Injection, XSS, and CSRF, the attacks web services suffer most, actually work, tried hands-on, and the right way to defend against each.","md",{"date":1755,"field":1756,"tags":1757},"2026.10.09","backend",[1758,1759],"web-security","nodejs","\u002Fen\u002Fweb-security-sql-injection-xss-csrf",{"title":5,"description":1752},{"loc":1760},"en\u002Fweb-security-sql-injection-xss-csrf","dxC90k3DswgdPNZNS06CM96sSEWYV6wyYeOTzNzDGkA",1791547856172]