From 126a49185229f248eae8c485b3e16e029ebcb353 Mon Sep 17 00:00:00 2001 From: polo Date: Fri, 23 Oct 2026 23:29:59 +0200 Subject: =?UTF-8?q?audit=20claude=20code=201,=20validation=20formulaire=20?= =?UTF-8?q?toujours=20ok,=20=C3=A9chappement=20dans=20URL,=20quelques=20ht?= =?UTF-8?q?mlspecialchars=20manquants,=20fonction=20Security::validateURL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Controller/HeadFootController.php | 9 +++++++-- src/Controller/MenuAndPathsController.php | 22 ++++++++++++---------- src/Controller/ViewController.php | 3 ++- src/Service/FormValidation.php | 2 +- src/Service/Router.php | 4 ++-- src/Service/Security.php | 8 +++++++- src/Service/URL.php | 27 +++++++-------------------- src/View/HeaderBuilder.php | 4 ++-- src/View/NewBuilder.php | 2 +- "\303\240 faire.txt" | 4 +++- 10 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/Controller/HeadFootController.php b/src/Controller/HeadFootController.php index e39c07b..2bf210e 100644 --- a/src/Controller/HeadFootController.php +++ b/src/Controller/HeadFootController.php @@ -28,14 +28,19 @@ class HeadFootController $node_data = $model->getNode()->getNodeData(); // liens réseaux sociaux + $link = trim($json['new_text'] ?? ''); // trim n'aime pas recevoir un NULL if(in_array($params_array[1], NodeData::$social_networks)){ + if($link !== '' && !Security::validateURL($link)){ + return new JsonResponse(['success' => false, 'error' => 'invalid_url']); + } + $social = $node_data->getData()['social'] ?? []; - $social[$params_array[1]] = $json['new_text']; + $social[$params_array[1]] = $link; $node_data->updateData('social', $social); } // autres textes else{ - $node_data->updateData($params_array[1], $json['new_text']); // $params_array[1] n'est pas contrôlé + $node_data->updateData($params_array[1], $link); // $params_array[1] n'est pas contrôlé } $entityManager->flush(); diff --git a/src/Controller/MenuAndPathsController.php b/src/Controller/MenuAndPathsController.php index 1916f06..aa283b6 100644 --- a/src/Controller/MenuAndPathsController.php +++ b/src/Controller/MenuAndPathsController.php @@ -6,31 +6,33 @@ declare(strict_types=1); namespace App\Controller; use Doctrine\ORM\EntityManager; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use App\Entity\Page; use App\Model\Menu; use App\Model\Model; use App\Service\URL; +use App\Service\Security; use App\View\NavBuilder; use App\View\MenuBuilder; class MenuAndPathsController { - static public function newUrlMenuEntry(EntityManager $entityManager): RedirectResponse + static public function newUrlMenuEntry(EntityManager $entityManager, Request $request): RedirectResponse { Model::$menu = new Menu($entityManager); $previous_page = Model::$menu->findPageById((int)$_POST["location"]); // (int) à cause de declare(strict_types=1); $parent = $previous_page->getParent(); - $url_input = trim($_POST["url_input"]); // faire htmlspecialchars à l'affichage - if(!filter_var($url_input, FILTER_VALIDATE_URL) || !str_starts_with($url_input, 'http')){ + $url_input = trim($request->request->get("url_input")); // faire htmlspecialchars à l'affichage + if(!Security::validateURL($url_input)){ // utiliser une flash error - return new RedirectResponse((string)new URL(['page' => $_GET['from'], 'error' => 'invalide_url'])); + return new RedirectResponse((string)new URL(['page' => $request->query->get('from'), 'error' => 'invalide_url'])); } $page = new Page( - trim(htmlspecialchars($_POST["label_input"])), + trim(htmlspecialchars($request->request->get("label_input"))), $url_input, '', true, true, false, $previous_page->getPosition(), @@ -47,7 +49,7 @@ class MenuAndPathsController $entityManager->persist($page); $entityManager->flush(); - return new RedirectResponse((string)new URL(['page' => $_GET['from']])); + return new RedirectResponse((string)new URL(['page' => $request->query->get('from')])); } // on pourrait utiliser FormValidation ici @@ -62,7 +64,7 @@ class MenuAndPathsController elseif(!in_array($json['field'], ['url_name', 'url_content'])){ return new JsonResponse(['success' => false, 'message' => "champ invalide"]); } - elseif($json['field'] === 'url_content' && (!filter_var($url_data, FILTER_VALIDATE_URL) || !str_starts_with($url_data, 'http'))){ + elseif($json['field'] === 'url_content' && !Security::validateURL($url_data)){ return new JsonResponse(['success' => false, 'message' => "la chaîne envoyée n'est pas une URL valide"]); } else{ @@ -77,10 +79,10 @@ class MenuAndPathsController } } - static public function deleteUrlMenuEntry(EntityManager $entityManager): RedirectResponse + static public function deleteUrlMenuEntry(EntityManager $entityManager, Request $request): RedirectResponse { Model::$menu = new Menu($entityManager); - $page = Model::$menu->findPageById((int)$_POST["delete"]); + $page = Model::$menu->findPageById((int)$request->request->get("delete")); $parent = $page->getParent(); if($parent == null){ $parent = Model::$menu; @@ -91,7 +93,7 @@ class MenuAndPathsController $entityManager->remove($page); // suppression en BDD $entityManager->flush(); - return new RedirectResponse((string)new URL(['page' => $_GET['from']])); + return new RedirectResponse((string)new URL(['page' => $request->query->get('from')])); } static public function MoveOneLevelUp(EntityManager $entityManager, array $json): JsonResponse diff --git a/src/Controller/ViewController.php b/src/Controller/ViewController.php index 1f13861..5be4ffd 100644 --- a/src/Controller/ViewController.php +++ b/src/Controller/ViewController.php @@ -38,7 +38,8 @@ class ViewController{ else{ // mode création d'article // l'id du bloc et 'from=' sont vérifiés dans ArticleController::editorSubmit - if($request->query->get('id')[0] === 'n' && $request->query->has('from') && !empty($request->query->get('from'))){ + if(preg_match('/^n\d+\z/', $request->query->get('id')) // débute par 'n' puis des chiffres, \z au lieu de $ pour éviter un saut de ligne à la fin + && $request->query->has('from') && !empty($request->query->get('from'))){ NewBuilder::$new_article_mode = true; } } diff --git a/src/Service/FormValidation.php b/src/Service/FormValidation.php index d91aa12..9f4564f 100644 --- a/src/Service/FormValidation.php +++ b/src/Service/FormValidation.php @@ -46,7 +46,7 @@ class FormValidation public function validate(): bool { $this->error = ($this->callStrategy)(); // appel de la callback - return empty($this->errors); + return $this->error === null; } public function getError(): string diff --git a/src/Service/Router.php b/src/Service/Router.php index 98cecc5..3055099 100644 --- a/src/Service/Router.php +++ b/src/Service/Router.php @@ -347,11 +347,11 @@ class Router{ /* -- page Menu et chemins -- */ // création d'une entrée de menu avec une URL elseif($this->request->request->has("label_input") && $this->request->request->has("url_input") && $this->request->request->has("location")){ - return MenuAndPathsController::newUrlMenuEntry($this->entityManager); + return MenuAndPathsController::newUrlMenuEntry($this->entityManager, $this->request); } // suppression d'une entrée de menu avec une URL elseif($this->request->request->has('delete') && $this->request->request->has('x') && $this->request->request->has('y')){ // 2 params x et y sont là parce qu'on a cliqué sur une image - return MenuAndPathsController::deleteUrlMenuEntry($this->entityManager); + return MenuAndPathsController::deleteUrlMenuEntry($this->entityManager, $this->request); } diff --git a/src/Service/Security.php b/src/Service/Security.php index 0fbd8b7..bb04535 100644 --- a/src/Service/Security.php +++ b/src/Service/Security.php @@ -27,6 +27,12 @@ class Security return trim(htmLawed($chaine, self::$configHtmLawed, self::$specHtmLawed)); } + public static function validateURL(string $link): bool + { + return filter_var($link, FILTER_VALIDATE_URL) !== false // c'est une URL? + && in_array(strtolower(parse_url($link, PHP_URL_SCHEME) ?? ''), ['http', 'https'], true); // PHP_URL_SCHEME pour récupérer le protocole + } + public static function secureFileName(string $chaine): string { // sécuriser un nom avec chemin avec basename? @@ -83,7 +89,7 @@ class Security } // lien sans http:// -function fixLinks($data) +function fixLinks(string $data): string { // 1/ // si une adresse est de type "domaine.fr" sans le http:// devant, le comportement des navigateurs est de rechercher un fichier comme si mon adresse commençait par file:// diff --git a/src/Service/URL.php b/src/Service/URL.php index 035f572..e6f986c 100644 --- a/src/Service/URL.php +++ b/src/Service/URL.php @@ -58,33 +58,20 @@ class URL implements \Stringable public function setAnchor(string $anchor = ''): void { if($anchor != ''){ - $this->anchor = '#' . ltrim($anchor, '#'); + $this->anchor = '#' . rawurlencode(ltrim($anchor, '#'));// rawurlencode échappe les mêmes caractères } else{ $this->anchor = ''; } } - private function makeParams(): string - { - $output = ''; - $first = true; - - foreach($this->params as $key => $value) { - if($first){ - $output .= '?'; - $first = false; - } - else{ - $output .= '&'; - } - $output .= $key . '=' . $value; - } - return $output; - } - public function __toString(): string { - return self::$protocol . self::$host . self::$port . self::$path . $this->makeParams() . $this->anchor; + return self::$protocol . self::$host . self::$port . self::$path + . ($this->params ? '?' . http_build_query($this->params) : '') + . $this->anchor; + // http_build_query change un tableau comme ['key1' => 'value 1', 'key2' => 'value 2'] + // en chaine: key1=value+1&key2=value+2 + // les caractères ", ', <, >, &, #, = et espaces sont échappés (comme avec rawurlencode pour les ancres) } } \ No newline at end of file diff --git a/src/View/HeaderBuilder.php b/src/View/HeaderBuilder.php index 14f5ffa..307c0bb 100644 --- a/src/View/HeaderBuilder.php +++ b/src/View/HeaderBuilder.php @@ -85,7 +85,7 @@ class HeaderBuilder extends AbstractBuilder // boucle sur la liste complète de réseaux sociaux foreach(NodeData::$social_networks as $network){ $checked = (isset($social_show[$network]) && $social_show[$network]) ? 'checked' : ''; - $href = (isset($social[$network]) && $social[$network] !== '') ? 'href="' . $social[$network] . '"' : ''; + $href = (isset($social[$network]) && $social[$network] !== '') ? 'href="' . htmlspecialchars($social[$network]) . '"' : ''; $social_networks .= '
@@ -112,7 +112,7 @@ class HeaderBuilder extends AbstractBuilder // boucle sur les réseaux sociaux "activés" foreach(array_keys($social_show) as $network){ if($social_show[$network]){ - $href = (isset($social[$network]) && $social[$network] !== '') ? 'href="' . $social[$network] . '"' : ''; + $href = (isset($social[$network]) && $social[$network] !== '') ? 'href="' . htmlspecialchars($social[$network]) . '"' : ''; $social_networks .= '