[{"data":1,"prerenderedAt":706},["ShallowReactive",2],{"page-\u002Fapi\u002Fsecurite-et-limites":3,"pieces-api":664,"autour-\u002Fapi\u002Fsecurite-et-limites":689,"prerequis-\u002Fapi\u002Fsecurite-et-limites":693,"plan-api":695},{"id":4,"title":5,"body":6,"description":627,"extension":628,"ligne":629,"meta":630,"motsCles":631,"navigation":218,"niveau":636,"path":637,"prerequis":638,"ressources":640,"seo":651,"sources":652,"stem":662,"__hash__":663},"docs\u002Fapi\u002F06.securite-et-limites.md","Sécurité et limites",{"type":7,"value":8,"toc":618},"minimark",[9,14,27,31,49,52,76,82,86,97,330,343,354,364,368,390,393,401,412,458,461,484,487,491,501,541,555,569,573,580,588,592,614],[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",{},"Pourquoi une API publique est attaquée par n'importe qui.",[18,22,23],{},"Contrôler l'accès avec un middleware serveur et le bon code d'erreur.",[18,25,26],{},"Régler CORS, mettre en cache les lectures et limiter les abus.",[10,28,30],{"id":29},"une-api-est-ouverte-à-tous","Une API est ouverte à tous",[32,33,34,35,39,40,43,44,48],"p",{},"Ta route ",[36,37,38],"code",{},"\u002Fapi\u002Frecettes"," est une adresse publique. Les outils du navigateur, ",[36,41,42],{},"curl"," ou un script peuvent l'appeler sans passer par ton site. Ton interface n'est donc ",[45,46,47],"strong",{},"pas"," une protection : un champ désactivé dans un formulaire n'empêche pas d'envoyer la requête à la main.",[32,50,51],{},"Règle d'or : le serveur vérifie tout, toujours. Le chapitre 5 en pose la base (valider le corps et l'identifiant, ne reprendre que les champs attendus). Ajoute :",[15,53,54,64,70],{},[18,55,56,59,60,63],{},[45,57,58],{},"limiter les tailles"," : ",[36,61,62],{},"max(100)"," sur un texte, plafond sur un nombre ;",[18,65,66,69],{},[45,67,68],{},"ne pas renvoyer plus que nécessaire"," : pas de champ interne, pas de trace d'erreur ;",[18,71,72,75],{},[45,73,74],{},"donner des messages d'erreur neutres",", pas le détail de ta base de données.",[77,78,79],"caution",{},[32,80,81],{},"Ne construis jamais une requête de base de données en collant le texte reçu : utilise les requêtes paramétrées de ta bibliothèque.",[10,83,85],{"id":84},"contrôler-laccès","Contrôler l'accès",[32,87,88,89,92,93,96],{},"Certaines routes (créer, modifier, supprimer) ne doivent pas être ouvertes à n'importe qui. Un ",[45,90,91],{},"middleware"," serveur, un fichier de ",[36,94,95],{},"server\u002Fmiddleware\u002F",", s'exécute avant chaque requête. D'après la documentation, il ne doit rien renvoyer : il inspecte la requête ou lève une erreur.",[98,99,105],"pre",{"className":100,"code":101,"filename":102,"language":103,"meta":104,"style":104},"language-ts shiki shiki-themes github-light-default github-dark-default github-dark-default","export default defineEventHandler((event) => {\n  const modifie = ['POST', 'PATCH', 'DELETE'].includes(event.method)\n  if (!event.path.startsWith('\u002Fapi\u002Frecettes') || !modifie) return\n\n  const config = useRuntimeConfig(event)\n  const jeton = getHeader(event, 'authorization')\n\n  if (jeton !== `Bearer ${config.cleAdmin}`) {\n    throw createError({ status: 401, statusText: 'Non autorise' })\n  }\n})\n","server\u002Fmiddleware\u002Fauth.ts","ts","",[36,106,107,134,174,213,220,236,258,263,292,316,322],{"__ignoreMap":104},[108,109,112,116,119,123,127,130],"span",{"class":110,"line":111},"line",1,[108,113,115],{"class":114},"sOnC1","export",[108,117,118],{"class":114}," default",[108,120,122],{"class":121},"syCW7"," defineEventHandler",[108,124,126],{"class":125},"sIXEE","((event) ",[108,128,129],{"class":114},"=>",[108,131,133],{"class":132},"scfrF"," {\n",[108,135,137,140,144,147,150,154,157,160,162,165,168,171],{"class":110,"line":136},2,[108,138,139],{"class":114},"  const",[108,141,143],{"class":142},"s5iOY"," modifie",[108,145,146],{"class":114}," =",[108,148,149],{"class":132}," [",[108,151,153],{"class":152},"shWOn","'POST'",[108,155,156],{"class":132},", ",[108,158,159],{"class":152},"'PATCH'",[108,161,156],{"class":132},[108,163,164],{"class":152},"'DELETE'",[108,166,167],{"class":132},"].",[108,169,170],{"class":121},"includes",[108,172,173],{"class":132},"(event.method)\n",[108,175,177,180,183,186,189,192,195,198,201,204,207,210],{"class":110,"line":176},3,[108,178,179],{"class":114},"  if",[108,181,182],{"class":132}," (",[108,184,185],{"class":114},"!",[108,187,188],{"class":132},"event.path.",[108,190,191],{"class":121},"startsWith",[108,193,194],{"class":132},"(",[108,196,197],{"class":152},"'\u002Fapi\u002Frecettes'",[108,199,200],{"class":132},") ",[108,202,203],{"class":114},"||",[108,205,206],{"class":114}," !",[108,208,209],{"class":132},"modifie) ",[108,211,212],{"class":114},"return\n",[108,214,216],{"class":110,"line":215},4,[108,217,219],{"emptyLinePlaceholder":218},true,"\n",[108,221,223,225,228,230,233],{"class":110,"line":222},5,[108,224,139],{"class":114},[108,226,227],{"class":142}," config",[108,229,146],{"class":114},[108,231,232],{"class":121}," useRuntimeConfig",[108,234,235],{"class":132},"(event)\n",[108,237,239,241,244,246,249,252,255],{"class":110,"line":238},6,[108,240,139],{"class":114},[108,242,243],{"class":142}," jeton",[108,245,146],{"class":114},[108,247,248],{"class":121}," getHeader",[108,250,251],{"class":132},"(event, ",[108,253,254],{"class":152},"'authorization'",[108,256,257],{"class":132},")\n",[108,259,261],{"class":110,"line":260},7,[108,262,219],{"emptyLinePlaceholder":218},[108,264,266,268,271,274,277,280,283,286,289],{"class":110,"line":265},8,[108,267,179],{"class":114},[108,269,270],{"class":132}," (jeton ",[108,272,273],{"class":114},"!==",[108,275,276],{"class":152}," `Bearer ${",[108,278,279],{"class":132},"config",[108,281,282],{"class":152},".",[108,284,285],{"class":132},"cleAdmin",[108,287,288],{"class":152},"}`",[108,290,291],{"class":132},") {\n",[108,293,295,298,301,304,307,310,313],{"class":110,"line":294},9,[108,296,297],{"class":114},"    throw",[108,299,300],{"class":121}," createError",[108,302,303],{"class":132},"({ status: ",[108,305,306],{"class":142},"401",[108,308,309],{"class":132},", statusText: ",[108,311,312],{"class":152},"'Non autorise'",[108,314,315],{"class":132}," })\n",[108,317,319],{"class":110,"line":318},10,[108,320,321],{"class":132},"  }\n",[108,323,325,328],{"class":110,"line":324},11,[108,326,327],{"class":132},"}",[108,329,257],{"class":125},[32,331,332,333,335,336,339,340,282],{},"Pour déclarer ",[36,334,285],{},", procède comme au chapitre 4, dans ",[36,337,338],{},"runtimeConfig"," et ",[36,341,342],{},".env",[32,344,345,346,349,350,353],{},"Retiens la distinction : ",[45,347,348],{},"authentification"," (qui es-tu ? 401 si absente) et ",[45,351,352],{},"autorisation"," (as-tu le droit ? 403 si refusée). Pour de vrais comptes, utilise une solution éprouvée plutôt que de la réécrire.",[355,356,357],"note",{},[32,358,359,360,363],{},"Les ",[36,361,362],{},"routeRules"," ne sont pas une mesure de sécurité, rappelle la documentation de Nuxt : ne t'en sers pas pour protéger une route.",[10,365,367],{"id":366},"cors-qui-peut-tappeler-depuis-un-navigateur","CORS : qui peut t'appeler depuis un navigateur",[32,369,370,371,374,375,378,379,382,383,386,387,282],{},"Un navigateur applique la ",[45,372,373],{},"politique de même origine"," : un script chargé depuis ",[36,376,377],{},"autre-site.fr"," ne peut pas lire une réponse de ",[36,380,381],{},"monsite.fr"," sans accord. Le serveur donne cet accord avec l'en-tête ",[36,384,385],{},"Access-Control-Allow-Origin",". C'est le mécanisme ",[45,388,389],{},"CORS",[32,391,392],{},"Par exemple, MDN montre qu'une origine précise s'écrit ainsi :",[98,394,399],{"className":395,"code":397,"language":398},[396],"language-text","Access-Control-Allow-Origin: https:\u002F\u002Fmonsite.fr\n","text",[36,400,397],{"__ignoreMap":104},[32,402,403,404,407,408,411],{},"Le joker ",[36,405,406],{},"*"," autorise toutes les origines, mais seulement pour les requêtes sans identifiants. Dans Nuxt, la règle de route ",[36,409,410],{},"cors: true"," ajoute des en-têtes CORS :",[98,413,416],{"className":100,"code":414,"filename":415,"language":103,"meta":104,"style":104},"export default defineNuxtConfig({\n  routeRules: {\n    '\u002Fapi\u002F**': { cors: true }\n  }\n})\n","nuxt.config.ts",[36,417,418,430,435,449,453],{"__ignoreMap":104},[108,419,420,422,424,427],{"class":110,"line":111},[108,421,115],{"class":114},[108,423,118],{"class":114},[108,425,426],{"class":121}," defineNuxtConfig",[108,428,429],{"class":125},"({\n",[108,431,432],{"class":110,"line":136},[108,433,434],{"class":132},"  routeRules: {\n",[108,436,437,440,443,446],{"class":110,"line":176},[108,438,439],{"class":152},"    '\u002Fapi\u002F**'",[108,441,442],{"class":132},": { cors: ",[108,444,445],{"class":142},"true",[108,447,448],{"class":132}," }\n",[108,450,451],{"class":110,"line":215},[108,452,321],{"class":132},[108,454,455],{"class":110,"line":222},[108,456,457],{"class":125},"})\n",[32,459,460],{},"Deux idées reçues à corriger :",[15,462,463,473],{},[18,464,465,466,469,470,472],{},"CORS ne protège pas ton serveur : il commande seulement ce que ",[45,467,468],{},"le navigateur"," laisse lire. ",[36,471,42],{}," ignore CORS.",[18,474,475,476,479,480,483],{},"Tu n'as pas besoin de CORS pour que ",[45,477,478],{},"ton propre site"," appelle ",[45,481,482],{},"ta propre API"," : même origine.",[32,485,486],{},"N'ouvre CORS que si un autre site doit vraiment appeler ton API, et à l'origine voulue.",[10,488,490],{"id":489},"le-cache-répondre-plus-vite","Le cache : répondre plus vite",[32,492,493,494,497,498,500],{},"Si la liste des recettes change rarement, inutile de la recalculer à chaque visite. La règle ",[36,495,496],{},"swr"," de ",[36,499,362],{}," met la réponse en cache pour une durée en secondes :",[98,502,504],{"className":100,"code":503,"filename":415,"language":103,"meta":104,"style":104},"export default defineNuxtConfig({\n  routeRules: {\n    '\u002Fapi\u002Frecettes': { swr: 60 }\n  }\n})\n",[36,505,506,516,520,533,537],{"__ignoreMap":104},[108,507,508,510,512,514],{"class":110,"line":111},[108,509,115],{"class":114},[108,511,118],{"class":114},[108,513,426],{"class":121},[108,515,429],{"class":125},[108,517,518],{"class":110,"line":136},[108,519,434],{"class":132},[108,521,522,525,528,531],{"class":110,"line":176},[108,523,524],{"class":152},"    '\u002Fapi\u002Frecettes'",[108,526,527],{"class":132},": { swr: ",[108,529,530],{"class":142},"60",[108,532,448],{"class":132},[108,534,535],{"class":110,"line":215},[108,536,321],{"class":132},[108,538,539],{"class":110,"line":222},[108,540,457],{"class":125},[32,542,543,544,546,547,550,551,554],{},"Selon la documentation, ",[36,545,496],{}," ajoute des en-têtes de cache et met la réponse en cache sur le serveur ou un serveur relais pendant la durée donnée. MDN explique l'en-tête ",[36,548,549],{},"Cache-Control",", avec ",[36,552,553],{},"max-age"," pour la durée de fraîcheur.",[556,557,558],"warning",{},[32,559,560,561,564,565,568],{},"Ne mets jamais en cache une réponse propre à un utilisateur (son profil, ses favoris) : un autre visiteur pourrait la recevoir. Garde le cache pour les données identiques pour tous. Et sache que, avec un ",[36,562,563],{},"POST"," ou un ",[36,566,567],{},"DELETE",", la liste en cache peut rester ancienne jusqu'à la fin de sa durée.",[10,570,572],{"id":571},"limiter-les-abus","Limiter les abus",[32,574,575,576,579],{},"Un script peut envoyer mille requêtes à la seconde. Le code ",[45,577,578],{},"429 Too Many Requests"," signale « trop de requêtes en peu de temps ». Cette limitation de débit n'est pas réglée pour toi : tu la mets en place devant ton application (hébergeur, pare-feu) ou avec un module. Garde au moins ces réflexes :",[15,581,582,585],{},[18,583,584],{},"plafonne la taille des listes (pagination) au lieu de tout renvoyer ;",[18,586,587],{},"protège les routes qui coûtent cher, comme l'appel d'une API payante du chapitre 4.",[10,589,591],{"id":590},"à-retenir","À retenir",[15,593,594,597,600,603,611],{},[18,595,596],{},"Une API est publique : le serveur valide tout et ne se fie jamais à l'interface.",[18,598,599],{},"Un middleware serveur peut exiger une authentification : 401 si absente, 403 si refusée.",[18,601,602],{},"CORS décide ce qu'un navigateur laisse lire d'un autre site ; il ne protège pas le serveur.",[18,604,605,607,608,610],{},[36,606,362],{}," avec ",[36,609,496],{}," met les lectures communes en cache ; jamais de données personnelles.",[18,612,613],{},"Contre les abus, plafonne les tailles et prévois une limitation de débit (429) en amont.",[615,616,617],"style",{},"html pre.shiki code .sOnC1, html code.shiki .sOnC1{--shiki-light:#CF222E;--shiki-default:#FF7B72;--shiki-dark:#FF7B72}html pre.shiki code .syCW7, html code.shiki .syCW7{--shiki-light:#8250DF;--shiki-default:#D2A8FF;--shiki-dark:#D2A8FF}html pre.shiki code .sIXEE, html code.shiki .sIXEE{--shiki-light:#953800;--shiki-default:#FFA657;--shiki-dark:#FFA657}html pre.shiki code .scfrF, html code.shiki .scfrF{--shiki-light:#1F2328;--shiki-default:#E6EDF3;--shiki-dark:#E6EDF3}html pre.shiki code .s5iOY, html code.shiki .s5iOY{--shiki-light:#0550AE;--shiki-default:#79C0FF;--shiki-dark:#79C0FF}html pre.shiki code .shWOn, html code.shiki .shWOn{--shiki-light:#0A3069;--shiki-default:#A5D6FF;--shiki-dark:#A5D6FF}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);}",{"title":104,"searchDepth":136,"depth":176,"links":619},[620,621,622,623,624,625,626],{"id":12,"depth":136,"text":13},{"id":29,"depth":136,"text":30},{"id":84,"depth":136,"text":85},{"id":366,"depth":136,"text":367},{"id":489,"depth":136,"text":490},{"id":571,"depth":136,"text":572},{"id":590,"depth":136,"text":591},"Protéger une API : ne jamais faire confiance aux entrées, contrôler l'accès, régler CORS, mettre en cache et limiter les abus.","md","api",{},[389,632,348,91,633,496,362,634,635],"sécurité","cache","429","validation","avance","\u002Fapi\u002Fsecurite-et-limites",[639],"\u002Fapi\u002Fecrire-une-api-complete",[641,645,648],{"nom":642,"url":643,"type":644},"Partage des ressources entre origines (CORS) sur MDN","https:\u002F\u002Fdeveloper.mozilla.org\u002Ffr\u002Fdocs\u002FWeb\u002FHTTP\u002FGuides\u002FCORS","doc",{"nom":646,"url":647,"type":644},"Mise en cache HTTP sur MDN","https:\u002F\u002Fdeveloper.mozilla.org\u002Ffr\u002Fdocs\u002FWeb\u002FHTTP\u002FGuides\u002FCaching",{"nom":649,"url":650,"type":644,"anglais":218},"Modes de rendu et routeRules, documentation officielle de Nuxt","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fguide\u002Fconcepts\u002Frendering",{"title":5,"description":627},[653,655,657,659],{"nom":654,"url":643},"Partage des ressources entre origines, CORS (MDN)",{"nom":656,"url":647},"Mise en cache HTTP (MDN)",{"nom":658,"url":650},"Rendu et routeRules (en anglais)",{"nom":660,"url":661},"Le dossier server (en anglais)","https:\u002F\u002Fnuxt.com\u002Fdocs\u002F4.x\u002Fdirectory-structure\u002Fserver","api\u002F06.securite-et-limites","rb_T8s9zjiUaIrfa7dQeHTq9wGELVOYaKXA3FgLusKo",[665,669,673,677,681,684,685],{"path":666,"title":667,"stem":668},"\u002Fapi\u002Fcomprendre-les-api","Comprendre les API","api\u002F01.comprendre-les-api",{"path":670,"title":671,"stem":672},"\u002Fapi\u002Fmethodes-et-codes-de-statut","Méthodes et codes de statut","api\u002F02.methodes-et-codes-de-statut",{"path":674,"title":675,"stem":676},"\u002Fapi\u002Fconsommer-une-api-externe","Consommer une API externe","api\u002F03.consommer-une-api-externe",{"path":678,"title":679,"stem":680},"\u002Fapi\u002Fcacher-une-cle-d-api","Cacher une clé d'API","api\u002F04.cacher-une-cle-d-api",{"path":639,"title":682,"stem":683},"Écrire une API complète","api\u002F05.ecrire-une-api-complete",{"path":637,"title":5,"stem":662},{"path":686,"title":687,"stem":688},"\u002Fapi","API","api\u002Findex",[690,692],{"title":682,"path":639,"stem":683,"description":691,"children":-1},"Construire les cinq routes d'une API de recettes (lire, créer, modifier, supprimer) avec validation des entrées et erreurs claires.",null,[694],{"path":639,"title":682,"ligne":629},[696,698,699,700,702,703,704],{"path":666,"title":667,"niveau":697,"stem":668},"debutant",{"path":670,"title":671,"niveau":697,"stem":672},{"path":674,"title":675,"niveau":697,"stem":676},{"path":678,"title":679,"niveau":701,"stem":680},"intermediaire",{"path":639,"title":682,"niveau":701,"stem":683},{"path":637,"title":5,"niveau":636,"stem":662},{"path":686,"title":687,"niveau":705,"stem":688},"memo",1794583450362]