[{"data":1,"prerenderedAt":718},["ShallowReactive",2],{"page-\u002Fsql\u002Ftransactions-et-injection":3,"pieces-sql":676,"autour-\u002Fsql\u002Ftransactions-et-injection":701,"prerequis-\u002Fsql\u002Ftransactions-et-injection":705,"plan-sql":707},{"id":4,"title":5,"body":6,"description":652,"extension":653,"ligne":61,"meta":654,"motsCles":655,"navigation":83,"niveau":658,"path":659,"prerequis":660,"ressources":662,"seo":670,"sources":671,"stem":674,"__hash__":675},"docs\u002Fsql\u002F06.transactions-et-injection.md","Transactions et injection SQL",{"type":7,"value":8,"toc":642},"minimark",[9,14,27,31,40,52,56,190,198,210,226,230,233,293,302,306,309,353,364,400,407,414,418,428,473,491,498,558,561,578,582,593,602,606,638],[10,11,13],"h2",{"id":12},"ce-que-tu-vas-apprendre","Ce que tu vas apprendre",[15,16,17,21,24],"ul",{},[18,19,20],"li",{},"Regrouper plusieurs modifications dans une transaction qui réussit en entier ou pas du tout.",[18,22,23],{},"Comprendre comment fonctionne une injection SQL.",[18,25,26],{},"Écrire des requêtes paramétrées pour t'en protéger.",[10,28,30],{"id":29},"pourquoi-des-transactions","Pourquoi des transactions",[32,33,34,35,39],"p",{},"Tu veux enregistrer une recette et ses ingrédients. Cela demande plusieurs ",[36,37,38],"code",{},"INSERT",". Si le deuxième échoue, tu te retrouves avec une recette sans ingrédients.",[32,41,42,43,47,48,51],{},"Une ",[44,45,46],"strong",{},"transaction"," regroupe plusieurs commandes en un bloc indivisible : soit toutes réussissent, soit aucune n'a d'effet. C'est l'",[44,49,50],{},"atomicité",".",[10,53,55],{"id":54},"begin-commit-rollback","BEGIN, COMMIT, ROLLBACK",[57,58,63],"pre",{"className":59,"code":60,"language":61,"meta":62,"style":62},"language-sql shiki shiki-themes github-light-default github-dark-default github-dark-default","BEGIN;\n\nINSERT INTO recettes (titre, temps_minutes, categorie_id)\nVALUES ('Crêpes', 30, 3);\n\nINSERT INTO ingredients (recette_id, nom, quantite, unite)\nSELECT id, 'farine', 250, 'g' FROM recettes WHERE titre = 'Crêpes';\n\nCOMMIT;\n","sql","",[36,64,65,78,85,94,122,127,135,177,182],{"__ignoreMap":62},[66,67,70,74],"span",{"class":68,"line":69},"line",1,[66,71,73],{"class":72},"sOnC1","BEGIN",[66,75,77],{"class":76},"scfrF",";\n",[66,79,81],{"class":68,"line":80},2,[66,82,84],{"emptyLinePlaceholder":83},true,"\n",[66,86,88,91],{"class":68,"line":87},3,[66,89,90],{"class":72},"INSERT INTO",[66,92,93],{"class":76}," recettes (titre, temps_minutes, categorie_id)\n",[66,95,97,100,103,107,110,114,116,119],{"class":68,"line":96},4,[66,98,99],{"class":72},"VALUES",[66,101,102],{"class":76}," (",[66,104,106],{"class":105},"shWOn","'Crêpes'",[66,108,109],{"class":76},", ",[66,111,113],{"class":112},"s5iOY","30",[66,115,109],{"class":76},[66,117,118],{"class":112},"3",[66,120,121],{"class":76},");\n",[66,123,125],{"class":68,"line":124},5,[66,126,84],{"emptyLinePlaceholder":83},[66,128,130,132],{"class":68,"line":129},6,[66,131,90],{"class":72},[66,133,134],{"class":76}," ingredients (recette_id, nom, quantite, unite)\n",[66,136,138,141,144,147,149,152,154,157,160,163,166,169,172,175],{"class":68,"line":137},7,[66,139,140],{"class":72},"SELECT",[66,142,143],{"class":76}," id, ",[66,145,146],{"class":105},"'farine'",[66,148,109],{"class":76},[66,150,151],{"class":112},"250",[66,153,109],{"class":76},[66,155,156],{"class":105},"'g'",[66,158,159],{"class":72}," FROM",[66,161,162],{"class":76}," recettes ",[66,164,165],{"class":72},"WHERE",[66,167,168],{"class":76}," titre ",[66,170,171],{"class":72},"=",[66,173,174],{"class":105}," 'Crêpes'",[66,176,77],{"class":76},[66,178,180],{"class":68,"line":179},8,[66,181,84],{"emptyLinePlaceholder":83},[66,183,185,188],{"class":68,"line":184},9,[66,186,187],{"class":72},"COMMIT",[66,189,77],{"class":76},[32,191,192,194,195,197],{},[36,193,73],{}," ouvre la transaction. ",[36,196,187],{}," la valide : toutes les modifications deviennent définitives et visibles pour les autres. Tant que tu n'as pas validé, les autres connexions ne voient rien.",[32,199,200,201,203,204,207,208,51],{},"Pour tout annuler, remplace ",[36,202,187],{}," par ",[36,205,206],{},"ROLLBACK",". La base revient à l'état d'avant ",[36,209,73],{},[211,212,213],"note",{},[32,214,215,216,218,219,222,223,225],{},"Sans ",[36,217,73],{},", chaque commande est sa propre transaction, validée tout de suite. C'est pourquoi un ",[36,220,221],{},"DELETE"," sans ",[36,224,165],{}," ne peut pas s'annuler après coup.",[10,227,229],{"id":228},"une-transaction-pour-tester","Une transaction pour tester",[32,231,232],{},"Voici une manière sûre d'essayer une modification risquée :",[57,234,236],{"className":59,"code":235,"language":61,"meta":62,"style":62},"BEGIN;\nDELETE FROM ingredients WHERE recette_id = 2;\nSELECT COUNT(*) FROM ingredients;\nROLLBACK;\n",[36,237,238,244,265,287],{"__ignoreMap":62},[66,239,240,242],{"class":68,"line":69},[66,241,73],{"class":72},[66,243,77],{"class":76},[66,245,246,248,250,253,255,258,260,263],{"class":68,"line":80},[66,247,221],{"class":72},[66,249,159],{"class":72},[66,251,252],{"class":76}," ingredients ",[66,254,165],{"class":72},[66,256,257],{"class":76}," recette_id ",[66,259,171],{"class":72},[66,261,262],{"class":112}," 2",[66,264,77],{"class":76},[66,266,267,269,272,275,278,281,284],{"class":68,"line":87},[66,268,140],{"class":72},[66,270,271],{"class":112}," COUNT",[66,273,274],{"class":76},"(",[66,276,277],{"class":72},"*",[66,279,280],{"class":76},") ",[66,282,283],{"class":72},"FROM",[66,285,286],{"class":76}," ingredients;\n",[66,288,289,291],{"class":68,"line":96},[66,290,206],{"class":72},[66,292,77],{"class":76},[32,294,295,296,298,299,301],{},"Tu constates l'effet du ",[36,297,221],{}," dans ta session, puis ",[36,300,206],{}," annule tout. C'est le filet de sécurité évoqué au cours 02.",[10,303,305],{"id":304},"linjection-sql","L'injection SQL",[32,307,308],{},"Dans une application, la requête est souvent construite avec du texte saisi par un utilisateur. Imagine une recherche de recette :",[57,310,314],{"className":311,"code":312,"language":313,"meta":62,"style":62},"language-ts shiki shiki-themes github-light-default github-dark-default github-dark-default","const titre = req.query.titre\nconst sql = \"SELECT * FROM recettes WHERE titre = '\" + titre + \"'\"\n","ts",[36,315,316,330],{"__ignoreMap":62},[66,317,318,321,324,327],{"class":68,"line":69},[66,319,320],{"class":72},"const",[66,322,323],{"class":112}," titre",[66,325,326],{"class":72}," =",[66,328,329],{"class":76}," req.query.titre\n",[66,331,332,334,337,339,342,345,347,350],{"class":68,"line":80},[66,333,320],{"class":72},[66,335,336],{"class":112}," sql",[66,338,326],{"class":72},[66,340,341],{"class":105}," \"SELECT * FROM recettes WHERE titre = '\"",[66,343,344],{"class":72}," +",[66,346,168],{"class":76},[66,348,349],{"class":72},"+",[66,351,352],{"class":105}," \"'\"\n",[32,354,355,356,359,360,363],{},"Si l'utilisateur tape ",[36,357,358],{},"Tarte",", tout va bien. Mais s'il tape ",[36,361,362],{},"' OR '1'='1",", la requête devient :",[57,365,367],{"className":59,"code":366,"language":61,"meta":62,"style":62},"SELECT * FROM recettes WHERE titre = '' OR '1'='1'\n",[36,368,369],{"__ignoreMap":62},[66,370,371,373,376,378,380,382,384,386,389,392,395,397],{"class":68,"line":69},[66,372,140],{"class":72},[66,374,375],{"class":72}," *",[66,377,159],{"class":72},[66,379,162],{"class":76},[66,381,165],{"class":72},[66,383,168],{"class":76},[66,385,171],{"class":72},[66,387,388],{"class":105}," ''",[66,390,391],{"class":72}," OR",[66,393,394],{"class":105}," '1'",[66,396,171],{"class":72},[66,398,399],{"class":105},"'1'\n",[32,401,402,403,406],{},"La condition ",[36,404,405],{},"'1'='1'"," est toujours vraie : toute la table est renvoyée. Un attaquant peut aussi lire d'autres tables ou, selon les droits, supprimer des données.",[32,408,409,410,413],{},"C'est une ",[44,411,412],{},"injection SQL"," : du texte saisi par l'utilisateur est interprété comme du code SQL.",[10,415,417],{"id":416},"la-parade-les-requêtes-paramétrées","La parade : les requêtes paramétrées",[32,419,420,421,109,424,427],{},"On sépare la requête du texte de l'utilisateur. La requête contient des emplacements numérotés ",[36,422,423],{},"$1",[36,425,426],{},"$2",", et les valeurs voyagent à part :",[57,429,431],{"className":311,"code":430,"language":313,"meta":62,"style":62},"const resultat = await client.query(\n  'SELECT * FROM recettes WHERE titre = $1',\n  [titre]\n)\n",[36,432,433,455,463,468],{"__ignoreMap":62},[66,434,435,437,440,442,445,448,452],{"class":68,"line":69},[66,436,320],{"class":72},[66,438,439],{"class":112}," resultat",[66,441,326],{"class":72},[66,443,444],{"class":72}," await",[66,446,447],{"class":76}," client.",[66,449,451],{"class":450},"syCW7","query",[66,453,454],{"class":76},"(\n",[66,456,457,460],{"class":68,"line":80},[66,458,459],{"class":105},"  'SELECT * FROM recettes WHERE titre = $1'",[66,461,462],{"class":76},",\n",[66,464,465],{"class":68,"line":87},[66,466,467],{"class":76},"  [titre]\n",[66,469,470],{"class":68,"line":96},[66,471,472],{"class":76},")\n",[32,474,475,476,479,480,483,484,487,488,490],{},"Ici, ",[36,477,478],{},"client"," est une connexion créée avec la bibliothèque ",[36,481,482],{},"pg"," (node-postgres). La valeur de ",[36,485,486],{},"titre"," est toujours traitée comme une simple donnée, jamais comme du SQL. Même ",[36,489,362],{}," est cherché comme un titre de recette.",[32,492,493,494,497],{},"PostgreSQL propose lui-même ce mécanisme avec ",[36,495,496],{},"PREPARE"," :",[57,499,501],{"className":59,"code":500,"language":61,"meta":62,"style":62},"PREPARE recette_par_titre (text) AS\n  SELECT * FROM recettes WHERE titre = $1;\n\nEXECUTE recette_par_titre('Tarte aux pommes');\n",[36,502,503,516,541,545],{"__ignoreMap":62},[66,504,505,508,511,513],{"class":68,"line":69},[66,506,507],{"class":76},"PREPARE recette_par_titre (",[66,509,510],{"class":72},"text",[66,512,280],{"class":76},[66,514,515],{"class":72},"AS\n",[66,517,518,521,523,525,527,529,531,533,536,539],{"class":68,"line":80},[66,519,520],{"class":72},"  SELECT",[66,522,375],{"class":72},[66,524,159],{"class":72},[66,526,162],{"class":76},[66,528,165],{"class":72},[66,530,168],{"class":76},[66,532,171],{"class":72},[66,534,535],{"class":76}," $",[66,537,538],{"class":112},"1",[66,540,77],{"class":76},[66,542,543],{"class":68,"line":87},[66,544,84],{"emptyLinePlaceholder":83},[66,546,547,550,553,556],{"class":68,"line":96},[66,548,549],{"class":72},"EXECUTE",[66,551,552],{"class":76}," recette_par_titre(",[66,554,555],{"class":105},"'Tarte aux pommes'",[66,557,121],{"class":76},[32,559,560],{},"Une instruction préparée n'existe que pendant la session. Dans ton code, tu n'as pas besoin de l'écrire à la main : ta bibliothèque de base de données s'en charge.",[562,563,564],"warning",{},[32,565,566,567,569,570,109,572,575,576,51],{},"Ne construis jamais une requête en collant du texte venu de l'extérieur (formulaire, adresse, fichier). Cela vaut pour ",[36,568,140],{},", mais aussi pour ",[36,571,38],{},[36,573,574],{},"UPDATE"," et ",[36,577,221],{},[10,579,581],{"id":580},"autres-bonnes-pratiques","Autres bonnes pratiques",[15,583,584,587,590],{},[18,585,586],{},"Donne à ton application un compte de base avec les droits minimaux, pas un compte administrateur.",[18,588,589],{},"Valide aussi les données côté serveur : type attendu, longueur, valeurs permises.",[18,591,592],{},"Un paramètre remplace une valeur, pas un nom de table ou de colonne. Pour un tri choisi par l'utilisateur, compare-le à une liste de noms autorisés écrite dans ton code.",[594,595,596],"tip",{},[32,597,598,599,601],{},"Quand tu utilises un client prêt à l'emploi, vérifie dans sa documentation comment passer des paramètres. Si tu vois du texte collé dans une requête avec ",[36,600,349],{}," ou une interpolation de chaîne, c'est le signal d'alarme.",[10,603,605],{"id":604},"à-retenir","À retenir",[15,607,608,617,624,627,630],{},[18,609,610,611,613,614,616],{},"Une transaction (",[36,612,73],{}," ... ",[36,615,187],{},") fait réussir ou échouer plusieurs commandes ensemble.",[18,618,619,621,622,51],{},[36,620,206],{}," annule tout ce qui a été fait depuis ",[36,623,73],{},[18,625,626],{},"Hors transaction explicite, chaque commande est validée immédiatement.",[18,628,629],{},"L'injection SQL survient quand du texte d'utilisateur est collé dans une requête.",[18,631,632,633,109,635,637],{},"Les requêtes paramétrées (",[36,634,423],{},[36,636,426],{},") séparent le code SQL des données et neutralisent l'injection.",[639,640,641],"style",{},"html pre.shiki code .sOnC1, html code.shiki .sOnC1{--shiki-light:#CF222E;--shiki-default:#FF7B72;--shiki-dark:#FF7B72}html pre.shiki code .scfrF, html code.shiki .scfrF{--shiki-light:#1F2328;--shiki-default:#E6EDF3;--shiki-dark:#E6EDF3}html pre.shiki code .shWOn, html code.shiki .shWOn{--shiki-light:#0A3069;--shiki-default:#A5D6FF;--shiki-dark:#A5D6FF}html pre.shiki code .s5iOY, html code.shiki .s5iOY{--shiki-light:#0550AE;--shiki-default:#79C0FF;--shiki-dark:#79C0FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .syCW7, html code.shiki .syCW7{--shiki-light:#8250DF;--shiki-default:#D2A8FF;--shiki-dark:#D2A8FF}",{"title":62,"searchDepth":80,"depth":87,"links":643},[644,645,646,647,648,649,650,651],{"id":12,"depth":80,"text":13},{"id":29,"depth":80,"text":30},{"id":54,"depth":80,"text":55},{"id":228,"depth":80,"text":229},{"id":304,"depth":80,"text":305},{"id":416,"depth":80,"text":417},{"id":580,"depth":80,"text":581},{"id":604,"depth":80,"text":605},"Regrouper des modifications en un bloc tout ou rien avec les transactions, et protéger une application contre l'injection SQL grâce aux requêtes paramétrées.","md",{},[46,73,187,206,50,412,656,496,423,657],"requête paramétrée","sécurité","avance","\u002Fsql\u002Ftransactions-et-injection",[661],"\u002Fsql\u002Fagregats-et-index",[663,667],{"nom":664,"url":665,"type":666},"Documentation PostgreSQL en français, « Transactions »","https:\u002F\u002Fdocs.postgresql.fr\u002Fcurrent\u002Ftutorial-transactions.html","doc",{"nom":668,"url":669,"type":666,"anglais":83},"PostgreSQL, commande PREPARE (en anglais)","https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fsql-prepare.html",{"title":5,"description":652},[672,673],{"nom":664,"url":665},{"nom":668,"url":669},"sql\u002F06.transactions-et-injection","Ib2Tr3MJGu07f_z6OwhDvtT8PMcDI4lPp7Fda2_mqHI",[677,681,685,689,693,696,697],{"path":678,"title":679,"stem":680},"\u002Fsql\u002Fbase-relationnelle","La base relationnelle et SELECT","sql\u002F01.base-relationnelle",{"path":682,"title":683,"stem":684},"\u002Fsql\u002Fmodifier-les-donnees","Ajouter, modifier et supprimer des lignes","sql\u002F02.modifier-les-donnees",{"path":686,"title":687,"stem":688},"\u002Fsql\u002Fcreer-des-tables","Créer des tables","sql\u002F03.creer-des-tables",{"path":690,"title":691,"stem":692},"\u002Fsql\u002Fjointures","Les jointures","sql\u002F04.jointures",{"path":661,"title":694,"stem":695},"Agrégats et index","sql\u002F05.agregats-et-index",{"path":659,"title":5,"stem":674},{"path":698,"title":699,"stem":700},"\u002Fsql","SQL","sql\u002Findex",[702,704],{"title":694,"path":661,"stem":695,"description":703,"children":-1},"Calculer des totaux et des moyennes avec COUNT, SUM, AVG, GROUP BY et HAVING, puis accélérer les recherches avec un index.",null,[706],{"path":661,"title":694,"ligne":61},[708,710,711,712,714,715,716],{"path":678,"title":679,"niveau":709,"stem":680},"debutant",{"path":682,"title":683,"niveau":709,"stem":684},{"path":686,"title":687,"niveau":709,"stem":688},{"path":690,"title":691,"niveau":713,"stem":692},"intermediaire",{"path":661,"title":694,"niveau":713,"stem":695},{"path":659,"title":5,"niveau":658,"stem":674},{"path":698,"title":699,"niveau":717,"stem":700},"memo",1794583463436]