[{"data":1,"prerenderedAt":1159},["ShallowReactive",2],{"blog-vulnerabilities/prisma-raw-query-injection":3},{"id":4,"title":5,"body":6,"category":1128,"date":1129,"dateModified":1129,"description":1130,"draft":1131,"extension":1132,"faq":1133,"featured":1131,"headerVariant":1144,"image":1145,"keywords":1146,"meta":1147,"navigation":81,"ogDescription":1148,"ogTitle":1145,"path":1149,"readTime":1150,"schemaOrg":1151,"schemaType":1152,"seo":1153,"sitemap":1154,"stem":1155,"tags":1156,"twitterCard":1157,"__hash__":1158},"blog/blog/vulnerabilities/prisma-raw-query-injection.md","Prisma $executeRawUnsafe Security: When Raw Queries Reopen SQL Injection (2026)",{"type":7,"value":8,"toc":1115},"minimark",[9,13,137,148,181,186,189,205,215,218,261,267,276,280,361,375,396,400,407,417,420,492,499,508,525,529,540,585,599,603,606,751,856,920,924,927,994,1008,1023,1026,1080,1099,1111],[10,11,12],"p",{},"Your app has a table view with sortable columns. Someone asked Cursor or Claude for \"let users sort by any column\", and it shipped something like this:",[14,15,17],"code-block",{"label":16},"The pattern that shows up in almost every AI-generated table endpoint",[18,19,24],"pre",{"className":20,"code":21,"language":22,"meta":23,"style":23},"language-typescript shiki shiki-themes github-dark","// app/api/orders/route.ts\nconst { sortBy, dir } = await request.json();\n\nconst orders = await prisma.$queryRawUnsafe(\n  `SELECT * FROM \"Order\" WHERE \"userId\" = '${userId}' ORDER BY \"${sortBy}\" ${dir}`,\n);\n","typescript","",[25,26,27,36,76,83,105,131],"code",{"__ignoreMap":23},[28,29,32],"span",{"class":30,"line":31},"line",1,[28,33,35],{"class":34},"sAwPA","// app/api/orders/route.ts\n",[28,37,39,43,47,51,54,57,60,63,66,69,73],{"class":30,"line":38},2,[28,40,42],{"class":41},"snl16","const",[28,44,46],{"class":45},"s95oV"," { ",[28,48,50],{"class":49},"sDLfK","sortBy",[28,52,53],{"class":45},", ",[28,55,56],{"class":49},"dir",[28,58,59],{"class":45}," } ",[28,61,62],{"class":41},"=",[28,64,65],{"class":41}," await",[28,67,68],{"class":45}," request.",[28,70,72],{"class":71},"svObZ","json",[28,74,75],{"class":45},"();\n",[28,77,79],{"class":30,"line":78},3,[28,80,82],{"emptyLinePlaceholder":81},true,"\n",[28,84,86,88,91,94,96,99,102],{"class":30,"line":85},4,[28,87,42],{"class":41},[28,89,90],{"class":49}," orders",[28,92,93],{"class":41}," =",[28,95,65],{"class":41},[28,97,98],{"class":45}," prisma.",[28,100,101],{"class":71},"$queryRawUnsafe",[28,103,104],{"class":45},"(\n",[28,106,108,112,115,118,120,123,125,128],{"class":30,"line":107},5,[28,109,111],{"class":110},"sU2Wk","  `SELECT * FROM \"Order\" WHERE \"userId\" = '${",[28,113,114],{"class":45},"userId",[28,116,117],{"class":110},"}' ORDER BY \"${",[28,119,50],{"class":45},[28,121,122],{"class":110},"}\" ${",[28,124,56],{"class":45},[28,126,127],{"class":110},"}`",[28,129,130],{"class":45},",\n",[28,132,134],{"class":30,"line":133},6,[28,135,136],{"class":45},");\n",[10,138,139,140,143,144,147],{},"That runs. It passes review, because Prisma has a reputation for being injection-proof and the code says ",[25,141,142],{},"prisma."," right there at the front. It is also a SQL injection bug, and the ",[25,145,146],{},"Unsafe"," in the method name is the only warning you got.",[149,150,151],"tldr",{},[10,152,153,154,157,158,161,162,164,165,168,169,172,173,176,177,180],{},"Prisma's ",[25,155,156],{},"$queryRaw"," and ",[25,159,160],{},"$executeRaw"," are tagged templates that escape every variable, so they're genuinely safe. Their ",[25,163,146],{}," twins take a plain string and send whatever you concatenated. Prisma's docs limit all four to one statement per call, so nobody appends ",[25,166,167],{},"DROP TABLE",". The realistic damage is a rewritten ",[25,170,171],{},"WHERE"," clause on an ",[25,174,175],{},"UPDATE"," or ",[25,178,179],{},"DELETE"," that quietly matches every row.",[182,183,185],"h2",{"id":184},"why-the-unsafe-methods-exist-at-all","Why the Unsafe methods exist at all",[10,187,188],{},"This is the part most write-ups skip, and it explains why the methods keep appearing in code nobody meant to write insecurely.",[10,190,191,192,196,197,200,201,204],{},"SQL placeholders bind ",[193,194,195],"strong",{},"values",", not ",[193,198,199],{},"identifiers",". You cannot parameterize a column name, a table name, or a keyword like ",[25,202,203],{},"ASC",". Prisma's documentation is explicit about it:",[206,207,208],"blockquote",{},[10,209,210,211,214],{},"\"Template variables can only be used for data values (such as ",[25,212,213],{},"email"," in the example above). Variables cannot be used for identifiers such as column names, table names or database names, or for SQL keywords.\"",[10,216,217],{},"The docs then show the query that does not work:",[14,219,221],{"label":220},"What Prisma says will not work",[18,222,224],{"className":20,"code":223,"language":22,"meta":23,"style":23},"const myTable = \"user\";\nawait prisma.$queryRaw`SELECT * FROM ${myTable};`;\n",[25,225,226,241],{"__ignoreMap":23},[28,227,228,230,233,235,238],{"class":30,"line":31},[28,229,42],{"class":41},[28,231,232],{"class":49}," myTable",[28,234,93],{"class":41},[28,236,237],{"class":110}," \"user\"",[28,239,240],{"class":45},";\n",[28,242,243,246,248,250,253,256,259],{"class":30,"line":38},[28,244,245],{"class":41},"await",[28,247,98],{"class":45},[28,249,156],{"class":71},[28,251,252],{"class":110},"`SELECT * FROM ${",[28,254,255],{"class":45},"myTable",[28,257,258],{"class":110},"};`",[28,260,240],{"class":45},[10,262,263,264,266],{},"So the moment a feature needs a dynamic column, the safe API stops being an option and the docs point you at ",[25,265,101],{},". An AI agent asked for sortable columns, dynamic filters, or a generic search endpoint hits that wall in about four seconds and takes the exit. It isn't being reckless. It's the only method that compiles.",[268,269,270],"warning-box",{},[10,271,272,273,275],{},"Ask an agent to \"make the sort dynamic\" and it will reach for the Unsafe method without flagging it, because that is the correct API for the request as stated. The fix is to change the request: \"make the sort dynamic, validating ",[25,274,50],{}," against an allowlist of permitted column names.\"",[182,277,279],{"id":278},"the-four-methods-and-which-two-escape","The four methods, and which two escape",[281,282,283,302],"table",{},[284,285,286],"thead",{},[287,288,289,293,296,299],"tr",{},[290,291,292],"th",{},"Method",[290,294,295],{},"Input",[290,297,298],{},"Returns",[290,300,301],{},"Escapes variables",[303,304,305,321,334,348],"tbody",{},[287,306,307,312,315,318],{},[308,309,310],"td",{},[25,311,156],{},[308,313,314],{},"Tagged template",[308,316,317],{},"Rows",[308,319,320],{},"Yes",[287,322,323,327,329,332],{},[308,324,325],{},[25,326,160],{},[308,328,314],{},[308,330,331],{},"Row count",[308,333,320],{},[287,335,336,340,343,345],{},[308,337,338],{},[25,339,101],{},[308,341,342],{},"Plain string",[308,344,317],{},[308,346,347],{},"No",[287,349,350,355,357,359],{},[308,351,352],{},[25,353,354],{},"$executeRawUnsafe",[308,356,342],{},[308,358,331],{},[308,360,347],{},[10,362,363,364,367,368,371,372,374],{},"The ",[25,365,366],{},"query"," versions read. The ",[25,369,370],{},"execute"," versions write, which is why ",[25,373,354],{}," is the one worth finding first. A read bug leaks data. A write bug changes it.",[10,376,377,378,381,382,389,390,392,393,395],{},"Prisma's warning on the Unsafe methods is direct: \"If you use this method with user inputs (in other words, ",[25,379,380],{},"SELECT * FROM table WHERE columnName = ${userInput}","), then you open up the possibility for SQL injection attacks.\" The ",[383,384,388],"a",{"href":385,"rel":386},"https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/raw-queries",[387],"nofollow","raw queries docs"," add that \"wherever possible you should use the ",[25,391,160],{}," method instead of ",[25,394,354],{},"\".",[182,397,399],{"id":398},"what-an-attack-actually-looks-like-here","What an attack actually looks like here",[10,401,402,403,406],{},"Most SQL injection articles open with ",[25,404,405],{},"'; DROP TABLE users; --",". Against Prisma that payload fails, and it's worth knowing why before you write off a finding as unexploitable.",[10,408,409,410,413,414,416],{},"Prisma's docs state that all four raw methods \"can only run ",[193,411,412],{},"one"," query at a time\". ",[25,415,354],{}," explicitly does not support multiple queries in a single string. Append a second statement and the driver rejects the whole thing.",[10,418,419],{},"That constraint is narrower than it sounds. The attacker doesn't need a second statement. They need to change the meaning of the one you already sent.",[14,421,423],{"label":422},"A single-statement attack on $executeRawUnsafe",[18,424,426],{"className":20,"code":425,"language":22,"meta":23,"style":23},"// The endpoint: mark one of the current user's orders as cancelled\nawait prisma.$executeRawUnsafe(\n  `UPDATE \"Order\" SET status = 'cancelled' WHERE id = ${orderId} AND \"userId\" = '${userId}'`,\n);\n\n// Attacker sends orderId = \"1 OR 1=1 --\"\n// The database receives:\n//   UPDATE \"Order\" SET status = 'cancelled' WHERE id = 1 OR 1=1 --  AND \"userId\" = '...'\n// The comment kills the ownership check. Every order in the table is now cancelled.\n",[25,427,428,433,443,461,465,469,474,480,486],{"__ignoreMap":23},[28,429,430],{"class":30,"line":31},[28,431,432],{"class":34},"// The endpoint: mark one of the current user's orders as cancelled\n",[28,434,435,437,439,441],{"class":30,"line":38},[28,436,245],{"class":41},[28,438,98],{"class":45},[28,440,354],{"class":71},[28,442,104],{"class":45},[28,444,445,448,451,454,456,459],{"class":30,"line":78},[28,446,447],{"class":110},"  `UPDATE \"Order\" SET status = 'cancelled' WHERE id = ${",[28,449,450],{"class":45},"orderId",[28,452,453],{"class":110},"} AND \"userId\" = '${",[28,455,114],{"class":45},[28,457,458],{"class":110},"}'`",[28,460,130],{"class":45},[28,462,463],{"class":30,"line":85},[28,464,136],{"class":45},[28,466,467],{"class":30,"line":107},[28,468,82],{"emptyLinePlaceholder":81},[28,470,471],{"class":30,"line":133},[28,472,473],{"class":34},"// Attacker sends orderId = \"1 OR 1=1 --\"\n",[28,475,477],{"class":30,"line":476},7,[28,478,479],{"class":34},"// The database receives:\n",[28,481,483],{"class":30,"line":482},8,[28,484,485],{"class":34},"//   UPDATE \"Order\" SET status = 'cancelled' WHERE id = 1 OR 1=1 --  AND \"userId\" = '...'\n",[28,487,489],{"class":30,"line":488},9,[28,490,491],{"class":34},"// The comment kills the ownership check. Every order in the table is now cancelled.\n",[10,493,494,495,498],{},"One statement. No ",[25,496,497],{},"DROP",". Your entire orders table is cancelled, and the endpoint returns a row count instead of an error, so nothing in your logs looks like an attack until a customer emails.",[500,501,502],"danger-box",{},[10,503,504,507],{},[25,505,506],{},"$executeRaw*"," returns the number of affected rows. That number is your best detector. An endpoint that should only ever touch one row and returns 4,812 is not a performance anomaly.",[10,509,510,511,514,515,517,518,520,521,524],{},"The same trick on the sort endpoint from the top of this page reads data rather than destroying it. ",[25,512,513],{},"ORDER BY \"${sortBy}\""," with ",[25,516,50],{}," set to a subquery, or a ",[25,519,50],{}," that closes the quote and appends a ",[25,522,523],{},"UNION",", turns a table view into an arbitrary-read primitive against every table the connection can reach. Prisma connections are usually the database owner, so that is every table.",[182,526,528],{"id":527},"the-prismaraw-trapdoor","The Prisma.raw trapdoor",[10,530,531,532,535,536,539],{},"You can also reintroduce the bug inside the safe methods. ",[25,533,534],{},"Prisma.sql"," builds a parameterized fragment. ",[25,537,538],{},"Prisma.raw"," does not: it inserts its argument verbatim, no escaping.",[14,541,543],{"label":542},"Safe method, unsafe result",[18,544,546],{"className":20,"code":545,"language":22,"meta":23,"style":23},"// Looks parameterized. Is not.\nawait prisma.$queryRaw`SELECT * FROM \"Order\" ORDER BY ${Prisma.raw(sortBy)}`;\n",[25,547,548,553],{"__ignoreMap":23},[28,549,550],{"class":30,"line":31},[28,551,552],{"class":34},"// Looks parameterized. Is not.\n",[28,554,555,557,559,561,564,567,570,573,576,578,581,583],{"class":30,"line":38},[28,556,245],{"class":41},[28,558,98],{"class":45},[28,560,156],{"class":71},[28,562,563],{"class":110},"`SELECT * FROM \"Order\" ORDER BY ${",[28,565,566],{"class":45},"Prisma",[28,568,569],{"class":110},".",[28,571,572],{"class":71},"raw",[28,574,575],{"class":110},"(",[28,577,50],{"class":45},[28,579,580],{"class":110},")",[28,582,127],{"class":110},[28,584,240],{"class":45},[10,586,587,588,590,591,594,595,598],{},"Prisma's docs name this directly: \"Another way to make these methods vulnerable is misuse of the ",[25,589,538],{}," function.\" A ",[25,592,593],{},"grep"," for ",[25,596,597],{},"RawUnsafe"," misses this entirely, which is why the audit below searches for both.",[182,600,602],{"id":601},"how-to-fix-it","How to fix it",[10,604,605],{},"The fix is an allowlist. Not sanitization, not escaping, not a regex that strips quotes and semicolons. An allowlist of exact strings you decided to support.",[607,608,610,615,744],"step",{"number":609},"1",[611,612,614],"h3",{"id":613},"allowlist-the-identifier","Allowlist the identifier",[18,616,618],{"className":20,"code":617,"language":22,"meta":23,"style":23},"const SORTABLE = [\"createdAt\", \"total\", \"status\"] as const;\nconst DIRECTIONS = { asc: \"ASC\", desc: \"DESC\" } as const;\n\nconst column = SORTABLE.includes(sortBy) ? sortBy : \"createdAt\";\nconst direction = DIRECTIONS[dir] ?? \"DESC\";\n",[25,619,620,656,685,689,722],{"__ignoreMap":23},[28,621,622,624,627,629,632,635,637,640,642,645,648,651,654],{"class":30,"line":31},[28,623,42],{"class":41},[28,625,626],{"class":49}," SORTABLE",[28,628,93],{"class":41},[28,630,631],{"class":45}," [",[28,633,634],{"class":110},"\"createdAt\"",[28,636,53],{"class":45},[28,638,639],{"class":110},"\"total\"",[28,641,53],{"class":45},[28,643,644],{"class":110},"\"status\"",[28,646,647],{"class":45},"] ",[28,649,650],{"class":41},"as",[28,652,653],{"class":41}," const",[28,655,240],{"class":45},[28,657,658,660,663,665,668,671,674,677,679,681,683],{"class":30,"line":38},[28,659,42],{"class":41},[28,661,662],{"class":49}," DIRECTIONS",[28,664,93],{"class":41},[28,666,667],{"class":45}," { asc: ",[28,669,670],{"class":110},"\"ASC\"",[28,672,673],{"class":45},", desc: ",[28,675,676],{"class":110},"\"DESC\"",[28,678,59],{"class":45},[28,680,650],{"class":41},[28,682,653],{"class":41},[28,684,240],{"class":45},[28,686,687],{"class":30,"line":78},[28,688,82],{"emptyLinePlaceholder":81},[28,690,691,693,696,698,700,702,705,708,711,714,717,720],{"class":30,"line":85},[28,692,42],{"class":41},[28,694,695],{"class":49}," column",[28,697,93],{"class":41},[28,699,626],{"class":49},[28,701,569],{"class":45},[28,703,704],{"class":71},"includes",[28,706,707],{"class":45},"(sortBy) ",[28,709,710],{"class":41},"?",[28,712,713],{"class":45}," sortBy ",[28,715,716],{"class":41},":",[28,718,719],{"class":110}," \"createdAt\"",[28,721,240],{"class":45},[28,723,724,726,729,731,733,736,739,742],{"class":30,"line":107},[28,725,42],{"class":41},[28,727,728],{"class":49}," direction",[28,730,93],{"class":41},[28,732,662],{"class":49},[28,734,735],{"class":45},"[dir] ",[28,737,738],{"class":41},"??",[28,740,741],{"class":110}," \"DESC\"",[28,743,240],{"class":45},[10,745,746,747,750],{},"Note that ",[25,748,749],{},"direction"," is looked up in a map rather than validated. The value that reaches the query is one you wrote, so there's nothing left for an attacker to influence.",[607,752,754,758,761,846],{"number":753},"2",[611,755,757],{"id":756},"parameterize-everything-that-is-a-value","Parameterize everything that is a value",[10,759,760],{},"Identifiers need the allowlist. Values do not, and mixing the two approaches is where people slip. Once the column is safe, put the values back in a tagged template:",[18,762,764],{"className":20,"code":763,"language":22,"meta":23,"style":23},"const orders = await prisma.$queryRaw`\n  SELECT * FROM \"Order\"\n  WHERE \"userId\" = ${userId}\n  ORDER BY ${Prisma.raw(`\"${column}\"`)} ${Prisma.raw(direction)}\n`;\n",[25,765,766,783,788,798,839],{"__ignoreMap":23},[28,767,768,770,772,774,776,778,780],{"class":30,"line":31},[28,769,42],{"class":41},[28,771,90],{"class":49},[28,773,93],{"class":41},[28,775,65],{"class":41},[28,777,98],{"class":45},[28,779,156],{"class":71},[28,781,782],{"class":110},"`\n",[28,784,785],{"class":30,"line":38},[28,786,787],{"class":110},"  SELECT * FROM \"Order\"\n",[28,789,790,793,795],{"class":30,"line":78},[28,791,792],{"class":110},"  WHERE \"userId\" = ${",[28,794,114],{"class":45},[28,796,797],{"class":110},"}\n",[28,799,800,803,805,807,809,811,814,817,820,822,825,827,829,831,833,835,837],{"class":30,"line":85},[28,801,802],{"class":110},"  ORDER BY ${",[28,804,566],{"class":45},[28,806,569],{"class":110},[28,808,572],{"class":71},[28,810,575],{"class":110},[28,812,813],{"class":110},"`\"${",[28,815,816],{"class":45},"column",[28,818,819],{"class":110},"}\"`",[28,821,580],{"class":110},[28,823,824],{"class":110},"} ${",[28,826,566],{"class":45},[28,828,569],{"class":110},[28,830,572],{"class":71},[28,832,575],{"class":110},[28,834,749],{"class":45},[28,836,580],{"class":110},[28,838,797],{"class":110},[28,840,841,844],{"class":30,"line":107},[28,842,843],{"class":110},"`",[28,845,240],{"class":45},[10,847,848,850,851,157,853,855],{},[25,849,114],{}," is escaped by Prisma. ",[25,852,816],{},[25,854,749],{}," are safe because they came out of your own constants, not the request.",[607,857,859,863,869,914],{"number":858},"3",[611,860,862],{"id":861},"ask-whether-you-needed-raw-sql","Ask whether you needed raw SQL",[10,864,865,866,868],{},"A surprising share of ",[25,867,101],{}," calls in AI-generated code exist because the agent didn't know Prisma's own API covered the case. Dynamic sorting is supported by the query builder:",[18,870,872],{"className":20,"code":871,"language":22,"meta":23,"style":23},"const orders = await prisma.order.findMany({\n  where: { userId },\n  orderBy: { [column]: direction.toLowerCase() },\n});\n",[25,873,874,893,898,909],{"__ignoreMap":23},[28,875,876,878,880,882,884,887,890],{"class":30,"line":31},[28,877,42],{"class":41},[28,879,90],{"class":49},[28,881,93],{"class":41},[28,883,65],{"class":41},[28,885,886],{"class":45}," prisma.order.",[28,888,889],{"class":71},"findMany",[28,891,892],{"class":45},"({\n",[28,894,895],{"class":30,"line":38},[28,896,897],{"class":45},"  where: { userId },\n",[28,899,900,903,906],{"class":30,"line":78},[28,901,902],{"class":45},"  orderBy: { [column]: direction.",[28,904,905],{"class":71},"toLowerCase",[28,907,908],{"class":45},"() },\n",[28,910,911],{"class":30,"line":85},[28,912,913],{"class":45},"});\n",[10,915,916,917,919],{},"The allowlist still matters, since ",[25,918,816],{}," becomes an object key. But there's no SQL string to get wrong, and Prisma rejects an unknown field outright.",[182,921,923],{"id":922},"auditing-what-your-agent-wrote","Auditing what your agent wrote",[10,925,926],{},"Two greps, both worth running. The second one is the one people forget.",[14,928,930],{"label":929},"Find every raw query in the codebase",[18,931,935],{"className":932,"code":933,"language":934,"meta":23,"style":23},"language-bash shiki shiki-themes github-dark","# The obvious one\ngrep -rn 'RawUnsafe' --include='*.ts' --include='*.js' .\n\n# The one that hides inside \"safe\" tagged templates\ngrep -rn 'Prisma\\.raw' --include='*.ts' --include='*.js' .\n","bash",[25,936,937,942,966,970,975],{"__ignoreMap":23},[28,938,939],{"class":30,"line":31},[28,940,941],{"class":34},"# The obvious one\n",[28,943,944,946,949,952,955,958,960,963],{"class":30,"line":38},[28,945,593],{"class":71},[28,947,948],{"class":49}," -rn",[28,950,951],{"class":110}," 'RawUnsafe'",[28,953,954],{"class":49}," --include=",[28,956,957],{"class":110},"'*.ts'",[28,959,954],{"class":49},[28,961,962],{"class":110},"'*.js'",[28,964,965],{"class":110}," .\n",[28,967,968],{"class":30,"line":78},[28,969,82],{"emptyLinePlaceholder":81},[28,971,972],{"class":30,"line":85},[28,973,974],{"class":34},"# The one that hides inside \"safe\" tagged templates\n",[28,976,977,979,981,984,986,988,990,992],{"class":30,"line":107},[28,978,593],{"class":71},[28,980,948],{"class":49},[28,982,983],{"class":110}," 'Prisma\\.raw'",[28,985,954],{"class":49},[28,987,957],{"class":110},[28,989,954],{"class":49},[28,991,962],{"class":110},[28,993,965],{"class":110},[10,995,996,997,1000,1001,53,1004,1007],{},"For each hit, the question isn't \"is this escaped\". It's: ",[193,998,999],{},"can any part of this string be influenced by a request?"," Trace the variable back to its source. If it came from ",[25,1002,1003],{},"req.body",[25,1005,1006],{},"searchParams",", a header, or a database column that a user controls, it needs the allowlist treatment.",[1009,1010,1012],"prompt-box",{"title":1011},"Audit prompt for an AI agent",[10,1013,1014,1015,53,1017,1019,1020,1022],{},"Find every call to ",[25,1016,101],{},[25,1018,354],{},", and ",[25,1021,538],{}," in this codebase. For each one, trace every interpolated variable back to its origin and tell me whether a user can influence it through a request body, query string, header, or stored value. Report the file, the line, and the traced path. Do not change any code yet.",[10,1024,1025],{},"The \"do not change any code yet\" matters. An agent left to fix these on its own tends to wrap the input in a regex that strips semicolons and call it done, which stops the payload in the example above and none of the others.",[1027,1028,1029,1036,1047,1058,1068],"faq-section",{},[1030,1031,1033],"faq-item",{"question":1032},"Is $executeRawUnsafe safe if I validate the input first?",[10,1034,1035],{},"Only if you validate against an allowlist of exact permitted strings, not against a pattern. Checking that a column name contains no quotes or semicolons is a blocklist, and blocklists on SQL syntax lose. Compare the input to a hardcoded array of column names you actually support and reject anything that isn't in it.",[1030,1037,1039],{"question":1038},"Can an attacker DROP TABLE through $executeRawUnsafe?",[10,1040,1041,1042,176,1044,1046],{},"Not by appending a second statement. Prisma's docs state that all four raw methods can only run one query at a time, so the classic stacked payload fails. The realistic attack rewrites the single statement you already sent. On an ",[25,1043,175],{},[25,1045,179],{}," that means changing which rows it matches, which can be all of them.",[1030,1048,1050],{"question":1049},"What is the difference between $executeRaw and $executeRawUnsafe?",[10,1051,1052,1054,1055,1057],{},[25,1053,160],{}," is a tagged template. Prisma escapes every interpolated variable and sends a parameterized query. ",[25,1056,354],{}," takes an ordinary JavaScript string, so whatever you concatenated into it goes to the database as SQL. The names are the whole API contract.",[1030,1059,1061],{"question":1060},"Why does my code use $queryRawUnsafe for sorting?",[10,1062,1063,1064,1067],{},"Because SQL placeholders bind values, not identifiers. Prisma's docs say template variables cannot be used for column names, table names, or SQL keywords. A dynamic ",[25,1065,1066],{},"ORDER BY"," column can't be parameterized, so any AI-generated sortable endpoint reaches for the Unsafe method. That's the single most common reason it shows up in a vibe-coded codebase.",[1030,1069,1071],{"question":1070},"Is Prisma.raw safe to use inside $queryRaw?",[10,1072,1073,1074,1076,1077,1079],{},"No. ",[25,1075,538],{}," inserts its argument verbatim with no escaping, which is exactly the behaviour the tagged template exists to prevent. Prisma's own docs call misuse of ",[25,1078,538],{}," a way to make the safe methods vulnerable. Use it only with strings you wrote yourself, never with anything derived from a request.",[1081,1082,1083,1089,1094],"related-articles",{},[1084,1085],"related-card",{"description":1086,"href":1087,"title":1088},"The full explainer: how injection works, what it costs, and why an ORM is not automatic immunity.","/blog/vulnerabilities/sql-injection","SQL Injection in Vibe-Coded Apps",[1084,1090],{"description":1091,"href":1092,"title":1093},"Access control middleware, field exposure, connection strings, and query limits across a whole Prisma app.","/blog/how-to/prisma-security","Prisma Security: A Step-by-Step Hardening Guide",[1084,1095],{"description":1096,"href":1097,"title":1098},"What parameterization does and does not cover, across Postgres, MySQL, and the common Node drivers.","/blog/how-to/parameterized-queries","How to Use Parameterized Queries",[1100,1101,1104,1108],"cta-box",{"href":1102,"label":1103},"/","Start Free Scan",[182,1105,1107],{"id":1106},"not-sure-what-your-api-is-exposing","Not sure what your API is exposing?",[10,1109,1110],{},"CheckYourVibe scans your deployed app for injection-prone endpoints, exposed admin paths, and secrets in client bundles. Free, and it takes about two minutes.",[1112,1113,1114],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#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);}",{"title":23,"searchDepth":38,"depth":38,"links":1116},[1117,1118,1119,1120,1121,1126,1127],{"id":184,"depth":38,"text":185},{"id":278,"depth":38,"text":279},{"id":398,"depth":38,"text":399},{"id":527,"depth":38,"text":528},{"id":601,"depth":38,"text":602,"children":1122},[1123,1124,1125],{"id":613,"depth":78,"text":614},{"id":756,"depth":78,"text":757},{"id":861,"depth":78,"text":862},{"id":922,"depth":38,"text":923},{"id":1106,"depth":38,"text":1107},"vulnerabilities","2026-08-17","Prisma escapes tagged-template variables, but $executeRawUnsafe takes a plain string. What that actually opens, why column names force it, and how to audit yours.",false,"md",[1134,1136,1138,1140,1142],{"question":1032,"answer":1135},"It is safe only if you validate against an allowlist of exact permitted strings, not against a pattern. Checking that a column name contains no quotes or semicolons is a blocklist, and blocklists on SQL syntax lose. Compare the input to a hardcoded array of column names you actually support and reject anything not in it.",{"question":1038,"answer":1137},"Not by appending a second statement. Prisma's docs state that all four raw methods can only run one query at a time, so the classic stacked-statement payload fails. The realistic attack rewrites the single statement you already sent. On an UPDATE or DELETE that means changing which rows it matches, which can hit every row in the table.",{"question":1049,"answer":1139},"$executeRaw is a tagged template. Prisma escapes every interpolated variable and sends a parameterized query. $executeRawUnsafe takes an ordinary JavaScript string, so whatever you concatenated into it goes to the database as SQL. The names are the whole API contract.",{"question":1060,"answer":1141},"Because SQL placeholders bind values, not identifiers. Prisma's docs say template variables cannot be used for column names, table names, or SQL keywords. A dynamic ORDER BY column cannot be parameterized, so any AI-generated sortable endpoint reaches for the Unsafe method. That is the single most common reason it appears in a vibe-coded codebase.",{"question":1070,"answer":1143},"No. Prisma.raw inserts its argument into the query verbatim with no escaping, which is exactly the behaviour the tagged template exists to prevent. Prisma's own docs call misuse of Prisma.raw a way to make the safe methods vulnerable. Use it only with strings you wrote yourself, never with anything derived from a request.","red",null,"prisma executerawunsafe security, prisma $executeRawUnsafe, prisma raw query sql injection, prisma queryRawUnsafe, prisma.raw sql injection, prisma orm vulnerability, prisma dynamic column name",{},"Prisma is safe by default until you hit a dynamic column name. Here is what $executeRawUnsafe opens and how to check your code.","/blog/vulnerabilities/prisma-raw-query-injection","9 min read","[object Object]","Article",{"title":5,"description":1130},{"loc":1149},"blog/vulnerabilities/prisma-raw-query-injection",[],"summary_large_image","X2ul2EVGzo7R0QkUi1tfyOkIAMwpWntk9TNzYjzV3lk",1787602622587]