[{"data":1,"prerenderedAt":1218},["ShallowReactive",2],{"blog-\u002Fblog\u002Fsql-injection-prevention":3},{"id":4,"title":5,"body":6,"description":1204,"difficulty":1205,"extension":1206,"framework":1207,"frameworkSlug":526,"meta":1208,"navigation":193,"order":50,"path":1209,"qaPath":1210,"seo":1211,"stem":1212,"subtopic":1213,"topic":1214,"topicSlug":1215,"updated":1216,"__hash__":1217},"blog\u002Fblog\u002Fsql-injection-prevention.md","SQL Injection — How It Works and How to Prevent It",{"type":7,"value":8,"toc":1192},"minimark",[9,14,18,26,30,113,125,129,132,321,325,328,515,519,522,692,696,699,818,825,829,832,995,999,1002,1010,1101,1105,1165,1181,1185,1188],[10,11,13],"h2",{"id":12},"what-sql-injection-is","What SQL injection is",[15,16,17],"p",{},"SQL injection (SQLi) is one of the oldest and most damaging web vulnerabilities.\nIt happens when an attacker supplies input that the application concatenates\ndirectly into a SQL string — and the database executes the injected SQL as if\nthe developer wrote it.",[15,19,20,21,25],{},"The consequences range from reading all rows in a table, bypassing login, to\ndestroying the entire database or running operating-system commands (via\n",[22,23,24],"code",{},"xp_cmdshell"," in SQL Server).",[10,27,29],{"id":28},"a-classic-example","A classic example",[31,32,37],"pre",{"className":33,"code":34,"language":35,"meta":36,"style":36},"language-python shiki shiki-themes github-light github-dark","# Vulnerable Python code — string concatenation with user input\nusername = request.form['username']  # attacker enters: ' OR '1'='1\nquery = \"SELECT * FROM users WHERE username = '\" + username + \"'\"\n# Resulting SQL:\n# SELECT * FROM users WHERE username = '' OR '1'='1'\n# → returns every user row; attacker is logged in as the first user\n","python","",[22,38,39,48,72,95,101,107],{"__ignoreMap":36},[40,41,44],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sJ8bj","# Vulnerable Python code — string concatenation with user input\n",[40,49,51,55,59,62,66,69],{"class":42,"line":50},2,[40,52,54],{"class":53},"sVt8B","username ",[40,56,58],{"class":57},"szBVR","=",[40,60,61],{"class":53}," request.form[",[40,63,65],{"class":64},"sZZnC","'username'",[40,67,68],{"class":53},"]  ",[40,70,71],{"class":46},"# attacker enters: ' OR '1'='1\n",[40,73,75,78,80,83,86,89,92],{"class":42,"line":74},3,[40,76,77],{"class":53},"query ",[40,79,58],{"class":57},[40,81,82],{"class":64}," \"SELECT * FROM users WHERE username = '\"",[40,84,85],{"class":57}," +",[40,87,88],{"class":53}," username ",[40,90,91],{"class":57},"+",[40,93,94],{"class":64}," \"'\"\n",[40,96,98],{"class":42,"line":97},4,[40,99,100],{"class":46},"# Resulting SQL:\n",[40,102,104],{"class":42,"line":103},5,[40,105,106],{"class":46},"# SELECT * FROM users WHERE username = '' OR '1'='1'\n",[40,108,110],{"class":42,"line":109},6,[40,111,112],{"class":46},"# → returns every user row; attacker is logged in as the first user\n",[15,114,115,116,120,121,124],{},"The attacker changed the SQL ",[117,118,119],"em",{},"structure",", not just the data. They closed the\nstring literal with ",[22,122,123],{},"'",", injected their own condition, and commented out the\nrest. The application thought it was passing data; the database received SQL.",[10,126,128],{"id":127},"parameterised-queries-the-primary-fix","Parameterised queries — the primary fix",[15,130,131],{},"A parameterised query (prepared statement) separates the SQL structure from the\ndata. The query template is sent first; data values are sent separately as\nbound parameters. The database always treats bound parameters as data — never\nas SQL syntax, regardless of what they contain.",[31,133,135],{"className":33,"code":134,"language":35,"meta":36,"style":36},"# Python + psycopg2 (Postgres) — SAFE\ncur.execute(\n    \"SELECT * FROM users WHERE username = %s AND password_hash = %s\",\n    (username, password_hash)\n)\n# Even if username = \"' OR '1'='1\", it is treated as a literal string.\n# The database looks for a user named exactly \"' OR '1'='1\" — finds none.\n\n# Node.js + pg (Postgres) — SAFE\nconst result = await client.query(\n    'SELECT * FROM orders WHERE customer_id = $1 AND status = $2',\n    [customerId, 'pending']\n);\n\n# Java JDBC — SAFE\nPreparedStatement stmt = conn.prepareStatement(\n    \"SELECT * FROM products WHERE category = ? AND unit_price \u003C ?\"\n);\nstmt.setString(1, category);\nstmt.setDouble(2, maxPrice);\nResultSet rs = stmt.executeQuery();\n",[22,136,137,142,147,167,172,177,182,188,195,201,215,223,235,245,250,256,267,273,280,294,308],{"__ignoreMap":36},[40,138,139],{"class":42,"line":43},[40,140,141],{"class":46},"# Python + psycopg2 (Postgres) — SAFE\n",[40,143,144],{"class":42,"line":50},[40,145,146],{"class":53},"cur.execute(\n",[40,148,149,152,156,159,161,164],{"class":42,"line":74},[40,150,151],{"class":64},"    \"SELECT * FROM users WHERE username = ",[40,153,155],{"class":154},"sj4cs","%s",[40,157,158],{"class":64}," AND password_hash = ",[40,160,155],{"class":154},[40,162,163],{"class":64},"\"",[40,165,166],{"class":53},",\n",[40,168,169],{"class":42,"line":97},[40,170,171],{"class":53},"    (username, password_hash)\n",[40,173,174],{"class":42,"line":103},[40,175,176],{"class":53},")\n",[40,178,179],{"class":42,"line":109},[40,180,181],{"class":46},"# Even if username = \"' OR '1'='1\", it is treated as a literal string.\n",[40,183,185],{"class":42,"line":184},7,[40,186,187],{"class":46},"# The database looks for a user named exactly \"' OR '1'='1\" — finds none.\n",[40,189,191],{"class":42,"line":190},8,[40,192,194],{"emptyLinePlaceholder":193},true,"\n",[40,196,198],{"class":42,"line":197},9,[40,199,200],{"class":46},"# Node.js + pg (Postgres) — SAFE\n",[40,202,204,207,209,212],{"class":42,"line":203},10,[40,205,206],{"class":53},"const result ",[40,208,58],{"class":57},[40,210,211],{"class":57}," await",[40,213,214],{"class":53}," client.query(\n",[40,216,218,221],{"class":42,"line":217},11,[40,219,220],{"class":64},"    'SELECT * FROM orders WHERE customer_id = $1 AND status = $2'",[40,222,166],{"class":53},[40,224,226,229,232],{"class":42,"line":225},12,[40,227,228],{"class":53},"    [customerId, ",[40,230,231],{"class":64},"'pending'",[40,233,234],{"class":53},"]\n",[40,236,238,241],{"class":42,"line":237},13,[40,239,240],{"class":53},")",[40,242,244],{"class":243},"s7hpK",";\n",[40,246,248],{"class":42,"line":247},14,[40,249,194],{"emptyLinePlaceholder":193},[40,251,253],{"class":42,"line":252},15,[40,254,255],{"class":46},"# Java JDBC — SAFE\n",[40,257,259,262,264],{"class":42,"line":258},16,[40,260,261],{"class":53},"PreparedStatement stmt ",[40,263,58],{"class":57},[40,265,266],{"class":53}," conn.prepareStatement(\n",[40,268,270],{"class":42,"line":269},17,[40,271,272],{"class":64},"    \"SELECT * FROM products WHERE category = ? AND unit_price \u003C ?\"\n",[40,274,276,278],{"class":42,"line":275},18,[40,277,240],{"class":53},[40,279,244],{"class":243},[40,281,283,286,289,292],{"class":42,"line":282},19,[40,284,285],{"class":53},"stmt.setString(",[40,287,288],{"class":154},"1",[40,290,291],{"class":53},", category)",[40,293,244],{"class":243},[40,295,297,300,303,306],{"class":42,"line":296},20,[40,298,299],{"class":53},"stmt.setDouble(",[40,301,302],{"class":154},"2",[40,304,305],{"class":53},", maxPrice)",[40,307,244],{"class":243},[40,309,311,314,316,319],{"class":42,"line":310},21,[40,312,313],{"class":53},"ResultSet rs ",[40,315,58],{"class":57},[40,317,318],{"class":53}," stmt.executeQuery()",[40,320,244],{"class":243},[10,322,324],{"id":323},"orm-queries-are-safe-raw-sql-escapes-are-not","ORM queries are safe — raw SQL escapes are not",[15,326,327],{},"Most ORMs use parameterised queries internally. The danger comes from their\nraw SQL escape hatches.",[31,329,331],{"className":33,"code":330,"language":35,"meta":36,"style":36},"# Django ORM — SAFE (parameterised internally)\norders = Order.objects.filter(customer_id=customer_id, status='pending')\n\n# Django raw() — UNSAFE (string concatenation)\norders = Order.objects.raw(\n    f\"SELECT * FROM orders WHERE customer_id = '{customer_id}'\"\n)\n\n# Django raw() — SAFE (bound parameters)\norders = Order.objects.raw(\n    \"SELECT * FROM orders WHERE customer_id = %s\",\n    [customer_id]\n)\n\n# SQLAlchemy text() — SAFE with bindparam\nfrom sqlalchemy import text\nresult = session.execute(\n    text(\"SELECT * FROM orders WHERE customer_id = :cid AND status = :st\"),\n    {\"cid\": customer_id, \"st\": \"pending\"}\n)\n",[22,332,333,338,366,370,375,384,403,407,411,416,424,435,440,444,448,453,467,477,488,511],{"__ignoreMap":36},[40,334,335],{"class":42,"line":43},[40,336,337],{"class":46},"# Django ORM — SAFE (parameterised internally)\n",[40,339,340,343,345,348,352,354,357,360,362,364],{"class":42,"line":50},[40,341,342],{"class":53},"orders ",[40,344,58],{"class":57},[40,346,347],{"class":53}," Order.objects.filter(",[40,349,351],{"class":350},"s4XuR","customer_id",[40,353,58],{"class":57},[40,355,356],{"class":53},"customer_id, ",[40,358,359],{"class":350},"status",[40,361,58],{"class":57},[40,363,231],{"class":64},[40,365,176],{"class":53},[40,367,368],{"class":42,"line":74},[40,369,194],{"emptyLinePlaceholder":193},[40,371,372],{"class":42,"line":97},[40,373,374],{"class":46},"# Django raw() — UNSAFE (string concatenation)\n",[40,376,377,379,381],{"class":42,"line":103},[40,378,342],{"class":53},[40,380,58],{"class":57},[40,382,383],{"class":53}," Order.objects.raw(\n",[40,385,386,389,392,395,397,400],{"class":42,"line":109},[40,387,388],{"class":57},"    f",[40,390,391],{"class":64},"\"SELECT * FROM orders WHERE customer_id = '",[40,393,394],{"class":154},"{",[40,396,351],{"class":53},[40,398,399],{"class":154},"}",[40,401,402],{"class":64},"'\"\n",[40,404,405],{"class":42,"line":184},[40,406,176],{"class":53},[40,408,409],{"class":42,"line":190},[40,410,194],{"emptyLinePlaceholder":193},[40,412,413],{"class":42,"line":197},[40,414,415],{"class":46},"# Django raw() — SAFE (bound parameters)\n",[40,417,418,420,422],{"class":42,"line":203},[40,419,342],{"class":53},[40,421,58],{"class":57},[40,423,383],{"class":53},[40,425,426,429,431,433],{"class":42,"line":217},[40,427,428],{"class":64},"    \"SELECT * FROM orders WHERE customer_id = ",[40,430,155],{"class":154},[40,432,163],{"class":64},[40,434,166],{"class":53},[40,436,437],{"class":42,"line":225},[40,438,439],{"class":53},"    [customer_id]\n",[40,441,442],{"class":42,"line":237},[40,443,176],{"class":53},[40,445,446],{"class":42,"line":247},[40,447,194],{"emptyLinePlaceholder":193},[40,449,450],{"class":42,"line":252},[40,451,452],{"class":46},"# SQLAlchemy text() — SAFE with bindparam\n",[40,454,455,458,461,464],{"class":42,"line":258},[40,456,457],{"class":57},"from",[40,459,460],{"class":53}," sqlalchemy ",[40,462,463],{"class":57},"import",[40,465,466],{"class":53}," text\n",[40,468,469,472,474],{"class":42,"line":269},[40,470,471],{"class":53},"result ",[40,473,58],{"class":57},[40,475,476],{"class":53}," session.execute(\n",[40,478,479,482,485],{"class":42,"line":275},[40,480,481],{"class":53},"    text(",[40,483,484],{"class":64},"\"SELECT * FROM orders WHERE customer_id = :cid AND status = :st\"",[40,486,487],{"class":53},"),\n",[40,489,490,493,496,499,502,505,508],{"class":42,"line":282},[40,491,492],{"class":53},"    {",[40,494,495],{"class":64},"\"cid\"",[40,497,498],{"class":53},": customer_id, ",[40,500,501],{"class":64},"\"st\"",[40,503,504],{"class":53},": ",[40,506,507],{"class":64},"\"pending\"",[40,509,510],{"class":53},"}\n",[40,512,513],{"class":42,"line":296},[40,514,176],{"class":53},[10,516,518],{"id":517},"stored-procedures-are-not-automatically-safe","Stored procedures are not automatically safe",[15,520,521],{},"A stored procedure protects against injection at the call site — the parameters\nare bound. But if the procedure builds dynamic SQL internally via string\nconcatenation, the injection surface moves inside the procedure.",[31,523,527],{"className":524,"code":525,"language":526,"meta":36,"style":36},"language-sql shiki shiki-themes github-light github-dark","-- SQL Server stored procedure — UNSAFE inside\nCREATE PROCEDURE SearchProducts @SearchTerm NVARCHAR(100)\nAS\nBEGIN\n    EXEC('SELECT * FROM products WHERE name LIKE ''%' + @SearchTerm + '%''')\n    -- Attacker passes: '; DROP TABLE products; --\nEND;\n\n-- SAFE: use sp_executesql with parameters\nCREATE PROCEDURE SearchProducts @SearchTerm NVARCHAR(100)\nAS\nBEGIN\n    DECLARE @sql NVARCHAR(500) = N'SELECT * FROM products WHERE name LIKE @t';\n    EXEC sp_executesql @sql, N'@t NVARCHAR(102)', @t = '%' + @SearchTerm + '%';\nEND;\n","sql",[22,528,529,534,556,561,566,588,593,600,604,609,625,629,633,658,686],{"__ignoreMap":36},[40,530,531],{"class":42,"line":43},[40,532,533],{"class":46},"-- SQL Server stored procedure — UNSAFE inside\n",[40,535,536,539,542,545,548,551,554],{"class":42,"line":50},[40,537,538],{"class":57},"CREATE",[40,540,541],{"class":57}," PROCEDURE",[40,543,544],{"class":53}," SearchProducts @SearchTerm ",[40,546,547],{"class":57},"NVARCHAR",[40,549,550],{"class":53},"(",[40,552,553],{"class":154},"100",[40,555,176],{"class":53},[40,557,558],{"class":42,"line":74},[40,559,560],{"class":57},"AS\n",[40,562,563],{"class":42,"line":97},[40,564,565],{"class":57},"BEGIN\n",[40,567,568,571,573,576,578,581,583,586],{"class":42,"line":103},[40,569,570],{"class":57},"    EXEC",[40,572,550],{"class":53},[40,574,575],{"class":64},"'SELECT * FROM products WHERE name LIKE ''%'",[40,577,85],{"class":57},[40,579,580],{"class":53}," @SearchTerm ",[40,582,91],{"class":57},[40,584,585],{"class":64}," '%'''",[40,587,176],{"class":53},[40,589,590],{"class":42,"line":109},[40,591,592],{"class":46},"    -- Attacker passes: '; DROP TABLE products; --\n",[40,594,595,598],{"class":42,"line":184},[40,596,597],{"class":57},"END",[40,599,244],{"class":53},[40,601,602],{"class":42,"line":190},[40,603,194],{"emptyLinePlaceholder":193},[40,605,606],{"class":42,"line":197},[40,607,608],{"class":46},"-- SAFE: use sp_executesql with parameters\n",[40,610,611,613,615,617,619,621,623],{"class":42,"line":203},[40,612,538],{"class":57},[40,614,541],{"class":57},[40,616,544],{"class":53},[40,618,547],{"class":57},[40,620,550],{"class":53},[40,622,553],{"class":154},[40,624,176],{"class":53},[40,626,627],{"class":42,"line":217},[40,628,560],{"class":57},[40,630,631],{"class":42,"line":225},[40,632,565],{"class":57},[40,634,635,638,641,643,645,648,651,653,656],{"class":42,"line":237},[40,636,637],{"class":57},"    DECLARE",[40,639,640],{"class":53}," @sql ",[40,642,547],{"class":57},[40,644,550],{"class":53},[40,646,647],{"class":154},"500",[40,649,650],{"class":53},") ",[40,652,58],{"class":57},[40,654,655],{"class":64}," N'SELECT * FROM products WHERE name LIKE @t'",[40,657,244],{"class":53},[40,659,660,662,665,668,671,673,676,678,680,682,684],{"class":42,"line":247},[40,661,570],{"class":57},[40,663,664],{"class":53}," sp_executesql @sql, ",[40,666,667],{"class":64},"N'@t NVARCHAR(102)'",[40,669,670],{"class":53},", @t ",[40,672,58],{"class":57},[40,674,675],{"class":64}," '%'",[40,677,85],{"class":57},[40,679,580],{"class":53},[40,681,91],{"class":57},[40,683,675],{"class":64},[40,685,244],{"class":53},[40,687,688,690],{"class":42,"line":252},[40,689,597],{"class":57},[40,691,244],{"class":53},[10,693,695],{"id":694},"second-order-injection","Second-order injection",[15,697,698],{},"Second-order injection is harder to spot. Malicious input is stored safely (the\nINSERT is parameterised) and later retrieved and used unsafely in a different\nquery.",[31,700,702],{"className":33,"code":701,"language":35,"meta":36,"style":36},"# Step 1: user registers with username = \"admin'--\"\n# Safely stored (parameterised INSERT) — no injection here\ncur.execute(\"INSERT INTO users (username) VALUES (%s)\", (username,))\n\n# Step 2: admin panel reads the username and uses it unsafely in a new query\nrow = db.fetchone(\"SELECT username FROM users WHERE id = %s\", (user_id,))\nadmin_username = row['username']   # → \"admin'--\"\n\n# UNSAFE: concatenates the stored value into a new query\ncur.execute(f\"SELECT * FROM audit_log WHERE actor = '{admin_username}'\")\n# → SELECT * FROM audit_log WHERE actor = 'admin'--'\n# Dumps all audit log rows\n",[22,703,704,709,714,730,734,739,759,777,781,786,808,813],{"__ignoreMap":36},[40,705,706],{"class":42,"line":43},[40,707,708],{"class":46},"# Step 1: user registers with username = \"admin'--\"\n",[40,710,711],{"class":42,"line":50},[40,712,713],{"class":46},"# Safely stored (parameterised INSERT) — no injection here\n",[40,715,716,719,722,724,727],{"class":42,"line":74},[40,717,718],{"class":53},"cur.execute(",[40,720,721],{"class":64},"\"INSERT INTO users (username) VALUES (",[40,723,155],{"class":154},[40,725,726],{"class":64},")\"",[40,728,729],{"class":53},", (username,))\n",[40,731,732],{"class":42,"line":97},[40,733,194],{"emptyLinePlaceholder":193},[40,735,736],{"class":42,"line":103},[40,737,738],{"class":46},"# Step 2: admin panel reads the username and uses it unsafely in a new query\n",[40,740,741,744,746,749,752,754,756],{"class":42,"line":109},[40,742,743],{"class":53},"row ",[40,745,58],{"class":57},[40,747,748],{"class":53}," db.fetchone(",[40,750,751],{"class":64},"\"SELECT username FROM users WHERE id = ",[40,753,155],{"class":154},[40,755,163],{"class":64},[40,757,758],{"class":53},", (user_id,))\n",[40,760,761,764,766,769,771,774],{"class":42,"line":184},[40,762,763],{"class":53},"admin_username ",[40,765,58],{"class":57},[40,767,768],{"class":53}," row[",[40,770,65],{"class":64},[40,772,773],{"class":53},"]   ",[40,775,776],{"class":46},"# → \"admin'--\"\n",[40,778,779],{"class":42,"line":190},[40,780,194],{"emptyLinePlaceholder":193},[40,782,783],{"class":42,"line":197},[40,784,785],{"class":46},"# UNSAFE: concatenates the stored value into a new query\n",[40,787,788,790,793,796,798,801,803,806],{"class":42,"line":203},[40,789,718],{"class":53},[40,791,792],{"class":57},"f",[40,794,795],{"class":64},"\"SELECT * FROM audit_log WHERE actor = '",[40,797,394],{"class":154},[40,799,800],{"class":53},"admin_username",[40,802,399],{"class":154},[40,804,805],{"class":64},"'\"",[40,807,176],{"class":53},[40,809,810],{"class":42,"line":217},[40,811,812],{"class":46},"# → SELECT * FROM audit_log WHERE actor = 'admin'--'\n",[40,814,815],{"class":42,"line":225},[40,816,817],{"class":46},"# Dumps all audit log rows\n",[15,819,820,821,824],{},"The fix: parameterise ",[117,822,823],{},"every"," query, including those that use data fetched from\nthe database. Data retrieved from the database must still be treated as untrusted.",[10,826,828],{"id":827},"dynamic-order-by-the-identifiers-problem","Dynamic ORDER BY — the identifiers problem",[15,830,831],{},"Bind parameters work for values but not for SQL identifiers (column names,\ntable names). Dynamic sort columns are a common injection vector.",[31,833,835],{"className":33,"code":834,"language":35,"meta":36,"style":36},"# UNSAFE: attacker sends sort='; DROP TABLE orders; --\nsort_col = request.args.get('sort')\ncur.execute(f\"SELECT * FROM orders ORDER BY {sort_col}\")\n\n# SAFE: allowlist of permitted column names\nALLOWED_SORT = {'id', 'created_at', 'total_amount', 'status'}\nsort_col = request.args.get('sort', 'created_at')\nif sort_col not in ALLOWED_SORT:\n    sort_col = 'created_at'   # safe fallback\n\ncur.execute(f\"SELECT * FROM orders ORDER BY {sort_col} DESC\")\n# sort_col is guaranteed to be from the allowlist, not user-supplied\n",[22,836,837,842,857,877,881,886,918,934,954,967,971,990],{"__ignoreMap":36},[40,838,839],{"class":42,"line":43},[40,840,841],{"class":46},"# UNSAFE: attacker sends sort='; DROP TABLE orders; --\n",[40,843,844,847,849,852,855],{"class":42,"line":50},[40,845,846],{"class":53},"sort_col ",[40,848,58],{"class":57},[40,850,851],{"class":53}," request.args.get(",[40,853,854],{"class":64},"'sort'",[40,856,176],{"class":53},[40,858,859,861,863,866,868,871,873,875],{"class":42,"line":74},[40,860,718],{"class":53},[40,862,792],{"class":57},[40,864,865],{"class":64},"\"SELECT * FROM orders ORDER BY ",[40,867,394],{"class":154},[40,869,870],{"class":53},"sort_col",[40,872,399],{"class":154},[40,874,163],{"class":64},[40,876,176],{"class":53},[40,878,879],{"class":42,"line":97},[40,880,194],{"emptyLinePlaceholder":193},[40,882,883],{"class":42,"line":103},[40,884,885],{"class":46},"# SAFE: allowlist of permitted column names\n",[40,887,888,891,894,897,900,903,906,908,911,913,916],{"class":42,"line":109},[40,889,890],{"class":154},"ALLOWED_SORT",[40,892,893],{"class":57}," =",[40,895,896],{"class":53}," {",[40,898,899],{"class":64},"'id'",[40,901,902],{"class":53},", ",[40,904,905],{"class":64},"'created_at'",[40,907,902],{"class":53},[40,909,910],{"class":64},"'total_amount'",[40,912,902],{"class":53},[40,914,915],{"class":64},"'status'",[40,917,510],{"class":53},[40,919,920,922,924,926,928,930,932],{"class":42,"line":184},[40,921,846],{"class":53},[40,923,58],{"class":57},[40,925,851],{"class":53},[40,927,854],{"class":64},[40,929,902],{"class":53},[40,931,905],{"class":64},[40,933,176],{"class":53},[40,935,936,939,942,945,948,951],{"class":42,"line":190},[40,937,938],{"class":57},"if",[40,940,941],{"class":53}," sort_col ",[40,943,944],{"class":57},"not",[40,946,947],{"class":57}," in",[40,949,950],{"class":154}," ALLOWED_SORT",[40,952,953],{"class":53},":\n",[40,955,956,959,961,964],{"class":42,"line":197},[40,957,958],{"class":53},"    sort_col ",[40,960,58],{"class":57},[40,962,963],{"class":64}," 'created_at'",[40,965,966],{"class":46},"   # safe fallback\n",[40,968,969],{"class":42,"line":203},[40,970,194],{"emptyLinePlaceholder":193},[40,972,973,975,977,979,981,983,985,988],{"class":42,"line":217},[40,974,718],{"class":53},[40,976,792],{"class":57},[40,978,865],{"class":64},[40,980,394],{"class":154},[40,982,870],{"class":53},[40,984,399],{"class":154},[40,986,987],{"class":64}," DESC\"",[40,989,176],{"class":53},[40,991,992],{"class":42,"line":225},[40,993,994],{"class":46},"# sort_col is guaranteed to be from the allowlist, not user-supplied\n",[10,996,998],{"id":997},"defence-in-depth","Defence in depth",[15,1000,1001],{},"Parameterised queries are the primary defence — they cannot be bypassed when\nused consistently. These layers reduce impact if a parameterisation is missed:",[31,1003,1008],{"className":1004,"code":1006,"language":1007},[1005],"language-text","1. Parameterised queries     — prevents injection (mandatory, non-negotiable)\n2. Input validation\u002Fallowlists — reduces attack surface\n3. Least-privilege DB user   — limits blast radius (no DROP TABLE, no pg_read_file)\n4. Error message suppression — hides schema info from error-based attackers\n5. WAF                       — catches automated scans (easily bypassed by humans)\n","text",[22,1009,1006],{"__ignoreMap":36},[31,1011,1013],{"className":33,"code":1012,"language":35,"meta":36,"style":36},"# Never return raw database errors to the client\ntry:\n    result = db.execute(query, params)\nexcept Exception as e:\n    logging.error(\"DB error: %s\", e, exc_info=True)   # log internally\n    return {\"error\": \"An internal error occurred\"}     # generic client message\n",[22,1014,1015,1020,1027,1037,1051,1080],{"__ignoreMap":36},[40,1016,1017],{"class":42,"line":43},[40,1018,1019],{"class":46},"# Never return raw database errors to the client\n",[40,1021,1022,1025],{"class":42,"line":50},[40,1023,1024],{"class":57},"try",[40,1026,953],{"class":53},[40,1028,1029,1032,1034],{"class":42,"line":74},[40,1030,1031],{"class":53},"    result ",[40,1033,58],{"class":57},[40,1035,1036],{"class":53}," db.execute(query, params)\n",[40,1038,1039,1042,1045,1048],{"class":42,"line":97},[40,1040,1041],{"class":57},"except",[40,1043,1044],{"class":154}," Exception",[40,1046,1047],{"class":57}," as",[40,1049,1050],{"class":53}," e:\n",[40,1052,1053,1056,1059,1061,1063,1066,1069,1071,1074,1077],{"class":42,"line":103},[40,1054,1055],{"class":53},"    logging.error(",[40,1057,1058],{"class":64},"\"DB error: ",[40,1060,155],{"class":154},[40,1062,163],{"class":64},[40,1064,1065],{"class":53},", e, ",[40,1067,1068],{"class":350},"exc_info",[40,1070,58],{"class":57},[40,1072,1073],{"class":154},"True",[40,1075,1076],{"class":53},")   ",[40,1078,1079],{"class":46},"# log internally\n",[40,1081,1082,1085,1087,1090,1092,1095,1098],{"class":42,"line":109},[40,1083,1084],{"class":57},"    return",[40,1086,896],{"class":53},[40,1088,1089],{"class":64},"\"error\"",[40,1091,504],{"class":53},[40,1093,1094],{"class":64},"\"An internal error occurred\"",[40,1096,1097],{"class":53},"}     ",[40,1099,1100],{"class":46},"# generic client message\n",[10,1102,1104],{"id":1103},"detecting-injection-vulnerabilities","Detecting injection vulnerabilities",[31,1106,1108],{"className":524,"code":1107,"language":526,"meta":36,"style":36},"-- Add injection payload tests to your test suite\n-- A search for \"' OR '1'='1\" should return zero products\ndef test_search_not_injectable():\n    results = search_products(\"' OR '1'='1\")\n    assert len(results) == 0  # not all products\n",[22,1109,1110,1115,1120,1125,1140],{"__ignoreMap":36},[40,1111,1112],{"class":42,"line":43},[40,1113,1114],{"class":46},"-- Add injection payload tests to your test suite\n",[40,1116,1117],{"class":42,"line":50},[40,1118,1119],{"class":46},"-- A search for \"' OR '1'='1\" should return zero products\n",[40,1121,1122],{"class":42,"line":74},[40,1123,1124],{"class":53},"def test_search_not_injectable():\n",[40,1126,1127,1130,1132,1135,1138],{"class":42,"line":97},[40,1128,1129],{"class":53},"    results ",[40,1131,58],{"class":57},[40,1133,1134],{"class":53}," search_products(",[40,1136,1137],{"class":64},"\"' OR '1'='1\"",[40,1139,176],{"class":53},[40,1141,1142,1145,1148,1151,1154,1157,1160,1162],{"class":42,"line":103},[40,1143,1144],{"class":53},"    assert ",[40,1146,1147],{"class":154},"len",[40,1149,1150],{"class":53},"(results) ",[40,1152,1153],{"class":57},"==",[40,1155,1156],{"class":154}," 0",[40,1158,1159],{"class":53},"  # ",[40,1161,944],{"class":57},[40,1163,1164],{"class":53}," all products\n",[15,1166,1167,1168,1172,1173,1176,1177,1180],{},"CI tools: ",[1169,1170,1171],"strong",{},"Bandit"," (Python SAST) flags unsafe DB calls; ",[1169,1174,1175],{},"Semgrep"," has rules\nfor SQL injection patterns; ",[1169,1178,1179],{},"sqlmap"," probes live endpoints black-box.",[10,1182,1184],{"id":1183},"recap","Recap",[15,1186,1187],{},"SQL injection is 100% preventable with parameterised queries — use them\neverywhere, without exception. ORM query APIs are safe by default; their raw\nSQL escapes are not. Stored procedures are only safe if their internal dynamic\nSQL is also parameterised. Treat data retrieved from the database as untrusted\nwhen building new queries (second-order injection). Allowlist sort\u002Ffilter\ncolumn names — identifiers cannot be bound as parameters. Defence in depth\n(least privilege, error suppression, WAF) limits the blast radius if injection\nsomehow occurs.",[1189,1190,1191],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":36,"searchDepth":50,"depth":50,"links":1193},[1194,1195,1196,1197,1198,1199,1200,1201,1202,1203],{"id":12,"depth":50,"text":13},{"id":28,"depth":50,"text":29},{"id":127,"depth":50,"text":128},{"id":323,"depth":50,"text":324},{"id":517,"depth":50,"text":518},{"id":694,"depth":50,"text":695},{"id":827,"depth":50,"text":828},{"id":997,"depth":50,"text":998},{"id":1103,"depth":50,"text":1104},{"id":1183,"depth":50,"text":1184},"SQL injection explained — how attacks work, parameterised queries, ORM safety, stored procedure risks, second-order injection, and a defence-in-depth checklist.","medium","md","SQL",{},"\u002Fblog\u002Fsql-injection-prevention","\u002Fsql\u002Fsecurity\u002Fsql-injection",{"title":5,"description":1204},"blog\u002Fsql-injection-prevention","SQL Injection","Security & Integrity","security","2026-06-20","qlwO42rDB6kaBLgKDy8-D0hyOvFtnvW0_PBJcGyrTOE",1782244088499]