
ngxray — nginx config security scanner
Analyseur statique de vulnérabilités pour les configurations nginx. Analyse les configurations avec le tokenizer natif de nginx et les compare à des règles JSON déclaratives pour détecter les CVE connues dans les motifs de directives du moteur de réécriture/script.
cd ngxray
git submodule update --init
make
python3 scan.py /etc/nginx/nginx.conf
Les règles sont définies sous forme de fichiers JSON dans rules/. Le scanner est livré avec :
| Règle | CVE | Motif |
|---|---|---|
rewrite-is-args | CVE-2026-42945 | rewrite … /path?args + set $var $1 |
rewrite-is-args-if | CVE-2026-42945 | rewrite … /path?args + if (… $1 …) |
nested-capture-redirect | CVE-2026-9256 | rewrite ^/((…))$ … redirect with overlapping $N |
is_args obsolèteAffecté : NGINX 0.6.27 – 1.30.0, NGINX Plus R32 – R36. Corrigé : 1.31.0 / 1.30.1.
Un remplacement rewrite contenant ? définit e->is_args=1 dans le moteur de script. Ce flag persiste dans les directives set ou if ultérieures, ce qui entraîne une échappement URI des données de capture (expansion 3x) lors de la passe de copie dans un tampon dimensionné pour les octets bruts.
location ~ ^/api/(.*)$ {
rewrite ^/api/(.*)$ /internal?migrated=true;
set $original_endpoint $1; # $1 évalué avec is_args=1 obsolète
}
Affecté : jusqu'à NGINX 1.31.0. Corrigé : 1.31.1 / 1.30.2.
Les groupes de capture imbriqués entraînent un sous-comptage du budget d'échappement du chemin rapide — les mêmes octets d'URI sont échappés une fois par référence $N qui se chevauche.
rewrite ^/((.*))$ http://backend/$1$2 redirect;
# Fichier unique
python3 scan.py /etc/nginx/nginx.conf
# Répertoire (récursif, tous les fichiers *.conf)
python3 scan.py /etc/nginx/
# Noms de fichiers depuis l'entrée standard
find /etc/nginx -name '*.conf' | python3 scan.py -
# JSONL pré-analysé (plus rapide pour les gros lots)
find /etc/nginx -name '*.conf' | ./build/nginx_conf_parse - | python3 scan.py --jsonl -
# Sortie JSON
python3 scan.py --json /etc/nginx/nginx.conf
# Répertoire de règles personnalisées
python3 scan.py --rules ./my-rules/ /etc/nginx/nginx.conf
Le paquet corpus_tools/ peut collecter du matériel de configuration nginx public à partir de GitHub Code Search et extraire des extraits nginx de formats d'encapsulation.
python3 corpus_tools/collect_github_nginx_corpus.py \
--output-dir corpus_out/nginx-rift \
--query-file corpus_tools/queries/nginx-rift.txt
python3 corpus_tools/extract_nginx_configs.py \
--input-dir corpus_out/nginx-rift/raw \
--output-dir corpus_out/nginx-rift/extracted
python3 scan.py corpus_out/nginx-rift/extracted
Consultez corpus_tools/README.md et corpus_tools/docs/ pour le workflow complet et la méthode de collecte documentée centrée sur Rift.
Déposez un fichier JSON dans rules/. Le moteur charge tous les fichiers *.json du répertoire de règles au démarrage.
{
"id": "nom-de-la-regle",
"cve": "CVE-YYYY-NNNNN",
"severity": "CRITICAL",
"message": "description courte affichée dans la sortie",
"affected": "plage de versions nginx",
"fixed": "versions corrigées",
"ref": "https://lien-vers-l-avis",
"match": [ ... ],
"tests": {
"vulnerable": ["server { ... }"],
"safe": ["server { ... }"]
}
}
Chaque règle peut inclure des tests avec des extraits de configuration vulnerable (doit déclencher) et safe (ne doit pas déclencher). Exécutez python3 scan.py --test pour valider toutes les règles.
match est une liste de conditions de directive. S'il y a une condition, le moteur recherche dans tout l'arbre de configuration toute directive correspondante. S'il y en a plusieurs, toutes les conditions doivent correspondre à des directives sœurs dans le même bloc.
Chaque condition :
{
"directive": "rewrite",
"args": {
"0": { "contains": "?" },
"1": { "regex": "\\$[1-9]" }
},
"any_arg": { "regex": "\\$[1-9]" },
"or": [
{ "args": { "1": { "contains": "?" } } },
{ "any_arg_from": 2, "any_of": ["redirect", "permanent"] }
]
}
args — indexé par position (sous forme de chaîne). Chaque valeur est un objet de condition.max_args — rejeter si la directive a plus de N arguments.overlapping_refs — vérifier que les références $N dans args[1] correspondent à des groupes de capture dans args[0] qui se contiennent physiquement.any_arg — la condition doit correspondre à au moins un argument à n'importe quelle position.or — au moins une branche doit correspondre. Les branches peuvent vérifier args ou any_arg_from + any_of.extract + min_unique est le mécanisme général pour compter les motifs dans un argument. Par exemple, pour exiger au moins 2 références de capture distinctes ($1–$9) :
{ "extract": "\\$([1-9])", "min_unique": 2 }
{
"id": "proxy-buffer-overflow",
"cve": "CVE-2099-99999",
"severity": "CRITICAL",
"message": "proxy_pass avec tampon surdimensionné — débordement de tas",
"ref": "https://example.com/advisory",
"match": [
{
"directive": "proxy_pass",
"args": { "0": { "regex": "https?://" } }
},
{
"directive": "proxy_buffer_size",
"args": { "0": { "regex": "^(6[5-9]|[7-9]\\d|\\d{3,})k$" } }
}
],
"tests": {
"vulnerable": [
"server { location / { proxy_pass http://backend; proxy_buffer_size 128k; } }"
],
"safe": [
"server { location / { proxy_pass http://backend; proxy_buffer_size 8k; } }"
]
}
}
Le répertoire parser/ contient un programme C autonome qui utilise le tokenizer propre à nginx (ngx_conf_read_token et ngx_conf_parse de src/core/ngx_conf_file.c) pour analyser les fichiers de configuration en un AST JSON. C'est le même analyseur lexical que nginx utilise au démarrage — pas de réimplémentation.
La modification clé : ngx_conf_handler() (qui distribue les directives aux modules compilés) est remplacé par conf_handler(), qui enregistre chaque directive dans un arbre indépendamment de son nom. Cela permet à l'analyseur de traiter toute configuration valide sans le système de modules. La modification est appliquée sous forme de correctif au moment de la construction — aucun fichier source nginx n'est copié dans ce dépôt.
Les fichiers source nginx restants (allocateur de pools, fonctions chaîne, journalisation, etc.) sont compilés directement à partir du sous-module.
scan.py charge les règles JSON depuis rules/, analyse les configurations via nginx_conf_parse, et évalue chaque règle par rapport à l'AST. Toutes les spécificités de détection résident dans les fichiers JSON de règles.
Nécessite un compilateur C et Python 3. Le source nginx est inclus comme sous-module git.
git submodule update --init
make
make test
Ce dépôt est mis en miroir en lecture seule vers github.com/califio/ngxray via Copybara. Voir COPYBARA.md.
| Clé | Type | Correspond quand |
|---|
contains | chaîne | L'argument contient la sous-chaîne |
regex | chaîne | L'argument correspond à l'expression régulière |
not_regex | chaîne | L'argument NE correspond PAS à l'expression régulière |
any_of | [chaîne] | L'argument est égal à l'une des valeurs |
extract | chaîne | Expression régulière avec un groupe de capture — extrait toutes les correspondances pour comptage |
min_unique | entier | (utilisé avec extract) Au moins N correspondances distinctes trouvées |