Que pourrait-il arriver de dangereux lorsque le mode SQL strict de MySQL est désactivé ?

Rédigé par Alexandre Zanni - 02/10/2025 - dans Pentest - Téléchargement

Cet article montre quelques exemples d'attaques qui peuvent abuser du comportement de MySQL lorsque le mode SQL strict est désactivé, en particulier lorsque les caractères de la chaîne sont invalides dans l'encodage actuel. Cela arrive quand l'encodage de l'application (ex : UTF-8) est plus large que celui de la base de données (ex : ASCII).

Vous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus

Qu'est-ce que le mode SQL strict de MySQL ?

Le mode strict[1] contrôle la manière dont MySQL gère les valeurs invalides ou manquantes dans les instructions de modification de données telles que INSERT ou UPDATE. Le cas détaillé dans cet article est celui où la valeur est en dehors de la plage valide. Mais que se passe-t-il lorsqu'une valeur est invalide pour l'encodage actuel ?

Le tableau suivant illustre de manière simplifiée le mode strict dans ce cas précis :

Mode strict activé

Mode strict désactivé

Erreur

Avertissement (silencieux)

Rien n'est modifié

Une valeur « ajustée » (« valeur la plus proche ») est insérée

À ce stade, vous devriez avoir deviné ce qui pourrait mal se passer.

Définir et vérifier

En fait, il n'existe pas de « mode strict » qui soit « activé » ou « désactivé ». Il existe une variable système appelée sql_mode qui contient un tableau de valeurs. Si ce tableau contient l'une des valeurs STRICT_TRANS_TABLES ou STRICT_ALL_TABLES ou le mode combiné TRADITIONAL, alors MySQL est en « mode strict ».

La documentation de MySQL 9.1[2] affirme que la valeur par défaut de sql_mode est la suivante.

ONLY_FULL_GROUP_BY STRICT_TRANS_TABLES NO_ZERO_IN_DATE NO_ZERO_DATE ERROR_FOR_DIVISION_BY_ZERO NO_ENGINE_SUBSTITUTION

La valeur par défaut peut différer pour d'autres versions ou d'autres moteurs tels que MariaDB ou Percona Server. La variable système @@sql_mode permet de consulter la valeur actuelle.

[u]> SELECT @@sql_mode\G;
@@sql_mode: STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION

Les programmes d'installation peuvent configurer le mode SQL au cours du processus d'installation. Ainsi, même si le mode strict est activé par défaut, XAMPP, les CMS, etc. peuvent le désactiver silencieusement.

La méthode violente pour désactiver le mode strict (et tout le reste par la même occasion) est la suivante :

SET sql_mode='';

Observations

Créons une table utilisant le codage ASCII, dans laquelle il est facile d'insérer des valeurs non valides, et voyons ce qui se passe lorsque le mode strict est activé ou non.

-- Créer une table avec une colonne utilisant volontairement un encodage « étroit »
CREATE TABLE uni_sandbox (
  id INT AUTO_INCREMENT PRIMARY KEY,
  data VARCHAR(255) CHARACTER SET ascii
);

-- Mode strict activé
SET sql_mode='STRICT_ALL_TABLES';
INSERT INTO uni_sandbox (data) VALUES ('I ♥ Unicode');
-- => ERROR 1366 (22007): Incorrect string value: '\xE2\x99\xA5 Un...' for column `unicode8`.`uni_sandbox`.`data` at row 1

-- Mode strict désactivé
SET sql_mode='';
INSERT INTO uni_sandbox (data) VALUES ('I ♥ Unicode');
SELECT * FROM uni_sandbox\G;
-- => data: I ? Unicode

Avec le mode strict activé, une erreur a été déclenchée. Avec le mode strict désactivé, les données ont été insérées, mais a été remplacé par ?. La documentation d'Oracle MySQL ou MariaDB indique « convertir la valeur invalide en la valeur valide la plus proche » sans expliquer exactement comment cela fonctionne. En pratique, les chaînes de caractères plus longues que la taille de la colonne seront tronquées, les entiers arrondis aux valeurs les plus proches, mais cela ne concerne que les valeurs qui débordent. Pour les valeurs non valides (qui ne peuvent être représentées dans l'encodage), chaque octet est remplacé par un ?.

Contexte d'attaque

Imaginons le contexte suivant : il existe une application web qui vérifie strictement les entrées de l'utilisateur. Une méthode générique couramment utilisée pour la validation consiste à utiliser des expressions régulières (RegExp). Indépendamment du langage, presque tous les moteurs RegExp prennent en charge les classes de caractères POSIX. Mais il y a quelques années, avec la généralisation de la prise en charge d'Unicode, de nombreux moteurs ont étendu la gamme de ces classes pour qu'elles fonctionnent également avec Unicode (à l'origine limité à ASCII). Ainsi, par exemple, au lieu de faire correspondre les caractères alphanumériques uniquement à la plage ASCII (A-Za-z0-9), le moteur fera également correspondre les caractères des catégories Unicode correspondantes telles que Lettre (L) et Chiffre (N) par défaut.

Prenons l'exemple suivant en Ruby avec le caractère Lettre latine percussion bidentale ʭ.

/[[:alnum:]]/.match('ʭ')
# => #<MatchData "ʭ">

D'autres langages, comme JavaScript, ont également mis en œuvre des sélecteurs de classes de caractères non-POSIX en implémentant des propriétés Unicode et des sélecteurs de catégories (\p{…}) où il est possible de faire correspondre directement les propriétés Unicode (ex : \p{Ll} ou Lowercase_Letter pour les lettres minuscules) ou des propriétés alias[3] (ex : \p{Alpha} pour Alphabétique qui correspond aux Lettres et Lettres numéros).

"ʭ".match(/\p{Alpha}/u)
// Array [ "ʭ" ]

D'autre part, si la base de données utilise un encodage « étroit » tel que ASCII, CP-1252 ou même une ancienne implémentation partielle telle que utf8mb3 en combinaison avec le mode strict désactivé (par exemple, l'installation du CMS le fait automatiquement), certains caractères Unicode passeraient le contrôle de sécurité mais, étant invalides dans l'encodage de la base de données, ils finiraient par être remplacés par des ?.

Dans ce contexte, quelques scénarios d'attaque et de contournement de la sécurité susceptibles de fonctionner dans la réalité viennent à l'esprit.

Exploitation du mode de repli SQL strict de MySQL

Résumé du contexte

Toutes les attaques présentées dans cette section seront placées dans le contexte suivant : l'application dispose de contrôles de sécurité n'autorisant que les caractères alphanumériques, y compris les caractères Unicode, mais la base de données MySQL utilise un encodage ASCII ou similaire dans lequel les caractères Unicode sont invalides et le mode SQL strict est désactivé.

Expansion de nom de fichier shell

Bash ne prend pas en charge les expressions régulières (RegExp), mais peut néanmoins procéder à une expansion des noms de fichiers, connue sous le nom de globbing en anglais. Bash étend alors les caractères connus sous le nom de caractère de remplacement (wild cards en anglais). Le plus connu est * qui correspond à n'importe quelle chaîne de n'importe quelle longueur, mais il y a aussi ? qui correspond exactement à un seul caractère (différent du quantificateur ? de RegExp). Par exemple, à la racine d'un système de fichiers Unix, ???t correspondra aux répertoires boot et root.

$ ls -d /???t
/boot  /root

Imaginons que l'on ait une lecture de fichier arbitraire grâce à un IDOR (Insecure Direct Object References) mais que les fichiers soient renommés avec un UUIDv4 (par exemple 2a0f6947-bd44-449e-94fe-82ebc3ecf115.txt). Techniquement, on pourrait lire les fichiers de tous les autres utilisateurs, mais en pratique, on ne le pourrait pas, car il n'est pas possible de mener une attaque par force brute sur l'identifiant.

Mais maintenant, que se passe-t-il si on demande de lire ʭʭʭʭʭʭʭ-ʭʭʭʭ-ʭʭʭʭ-ʭʭʭʭ-ʭʭʭʭʭʭʭʭʭʭʭʭ.txt ? Cela permettrait de lister tous les fichiers, car ʭ (lettre latine Bidental Percussive), ainsi que des centaines de milliers d'autres caractères, est un caractère Unicode de type Lettre qui passerait le contrôle de sécurité alphanumérique. Ensuite, lorsqu'il est stocké dans MySQL, il n'est pas reconnu comme un caractère ASCII valide, ce qui le ramène à ? car le mode SQL strict est désactivé. Ensuite, dans Bash, ? serait développé comme n'importe quel caractère unique. Grâce à cela, une commande find listerait tous les fichiers du répertoire au lieu d'un seul, contournant ainsi le contrôle de sécurité de l'application ainsi que l'utilisation de l'identifiant UUID.

Quantifier d'expression régulière

Pour les RegExps, ? est un caractère quantificateur, il est ajouté après une expression pour indiquer qu'il doit y avoir zéro ou une occurrence.

Par exemple, l'expression fichier\d?\.txt correspondra à n'importe quel nom de fichier ci-dessous ne comportant qu'un seul ou aucun chiffre, mais pas à fichier10.txt parce qu'il comporte deux chiffres.

fichier.txt
fichier1.txt
…
fichier9.txt

On peut imaginer une application Python qui renvoie des fichiers en fonction des données saisies par l'utilisateur, comme suit.

import re

username = User.username # données de l'utilisateur extraites de la base de données
check(username, lib.alphanum) # une vérification alphanumérique Unicode
re.findall(f'confidential-{username}\.pdf', 'list-files-fetched-fromFS-or-DB', re.IGNORECASE)

Ainsi, comme précédemment, l'enregistrement d'un nom d'utilisateur de aʭbʭ…yʭzʭ0ʭ1…8ʭ9ʭ serait transformé en a?b?…y?z?0?1?…8?9? ? ce qui permettrait de faire correspondre n'importe quelle lettre ASCII et n'importe quel chiffre une fois interpolé dans un RegExp. Si l'on vise un motif de cinq caractères, ce motif doit être répété 5 fois. La charge utile serait très longue et inefficace, mais elle fonctionnerait.

import re
import string

payload = "".join(map(lambda i: i + '?', string.ascii_lowercase + string.digits)) * 5
re.findall(f'confidential-{payload}\.pdf', 'confidential-noraj.pdf', re.IGNORECASE)
# => ['confidential-noraj.pdf']

Bien sûr, ce serait plus facile dans un scénario où il est autorisé d'utiliser des traits d'union et des parenthèses, et où il y aurait juste besoin de contourner la restriction du ?.

Paramètre de requête

La possibilité d'injecter un ? dans un contexte où l'utilisateur est censé ne pouvoir écrire que des caractères alphanumériques pourrait également permettre d'injecter un paramètre de requête dans une URL.

Par exemple, si une application web crée en interne une URL basée sur le nom de l'utilisateur ou sur toute entrée utilisateur filtrée stockée dans la base de données, l'utilisateur pourrait alors être en mesure d'ajouter un paramètre de requête tel que debug ou admin qui lui donnerait un accès non autorisé ou des informations sensibles. L'exemple partiel en Python ci-dessous peut donner une idée de ce à quoi cela pourrait ressembler.

from flask import Flask, redirect, url_for
import urllib.parse

app = Flask(__name__)

@app.route('/data/<username>')
@internal # Route interne pour obtenir les données utilisateur par nom de compte
def profile_name(username):
    # Obtient des données depuis le système de fichier basé sur le nom de compte
    data = "user_secret" if request.args.get("debug") != None else "user_public_stuff"
    return data

@app.route('/profile/name/<userid>')
@internal # Route interne pour convertir un identifiant utilisateur en nom de compte
def profile_data(userid):
    user = User(userid).username # Instancie la classe utilisateur pour récupérer les données depuis la BDD
    return redirect(urllib.parse.unquote(url_for('profile_name', username=user)))

@app.route('/api/public/profile/<userid>')
@auth # Route publique pour obtenir les informations de profil
def api_profile(userid):
    return redirect(url_for('profile_data', userid=userid))

@app.route('/api/public/profile/name/<userid>')
@auth # Route publique pour définir un nom d'utilisateur
def set_name(userid):
    username = request.args.get("name")
    check(username, lib.alphanum) # une vérification alphanumérique Unicode
    User(userid).username = username
    return True

if __name__ == '__main__':
    app.run(debug=True)

Avec ce qui a été vu précédemment, la base de données transformerait un nom d'utilisateur comme norajʭdebug en noraj?debug. Bien entendu, l'impossibilité d'injecter un = empêchera de passer une valeur au paramètre. Cependant, en fonction du framework web utilisé ou du langage de programmation, l'application peut seulement vérifier que le paramètre est présent.

Contournement de pare-feu applicatif (WAF)

En dehors du contexte défini précédemment, l'injection indirecte d'un ? peut bien sûr aussi aider à contourner les WAFs. Par exemple, dans une base de données stockant des blobs de fichiers ou des modèles auxquels l'attaquant a accès en écriture, le WAF bloquerait une charge utile comprenant <?php. Dans ce cas, l'envoi de <ʭphp à la place pourrait contourner la règle de détection et être converti en charge utile originale immédiatement après grâce au comportement de repli strict du mode SQL.

Histoire réelle

Il y a longtemps (en 2012), les planètes étaient alignées pour permettre l'apparition de bogues loufoques. À cette époque préhistorique, Wordpress désactivait le mode SQL strict lors de l'installation. D'autre part, MySQL enchaînait les défauts de conception. La seule implémentation d'UTF-8 dans MySQL s'appelait utf8 (qui s'appelle maintenant utf8mb3) et avait le problème de ne gérer que des caractères de 1 à 3 octets[4] (au lieu des 4 octets maximum pour de l'UTF-8 valide). utf8mb3 était très trompeur, car le fait de revendiquer que MySQL supportait l'UTF-8 laissait penser que le support de l'UTF-8 était complet à 100%, et pas seulement une implémentation partielle. L'autre comportement dangereux était que lorsque le mode SQL strict était désactivé, il ne remplaçait pas les caractères invalides par des ? comme c'est le cas actuellement. Au lieu de cela, les caractères invalides étaient purement et simplement supprimés, tout comme le reste de la chaîne ! Ainsi, tout caractère UTF-8 valide de 4 octets était considéré comme invalide dans utf8mb3 et déclenchait une troncation de tout ce qui suivait.

La combinaison de ces trois failles majeures a été exploitée en 2014 par Cedric's Cruft pour obtenir une XSS stockée dans Wordpress[5] dans la fonction principale de commentaire.