Règles de refus de chemins (.oxphpdeny)
Un fichier .oxphpdeny placé au sommet de DOCUMENT_ROOT liste les chemins de requête que le serveur ne doit pas servir directement. Il s'écrit dans la syntaxe de .gitignore, avec un ajout : une règle préfixée par > confie la requête au script d'entrée de l'application au lieu de la refuser.
# Never served — answered with PHP_DENY_FALLBACK (404 by default)
/vendor
/composer.*
*.sql
*.bak
# Not served directly — the front controller decides
> /storage/invoices/Il est destiné aux fichiers qu'une racine web ne devrait pas contenir mais contient souvent : vendor/ et composer.json dans une application héritée servie depuis la racine de son projet, les dumps de base de données et les sauvegardes, les artefacts de build et les source maps dans le public/ d'un framework. Les règles s'appliquent dans les quatre modes de routage, aux fichiers statiques comme aux scripts PHP. Elles sont vérifiées avant tout accès disque, si bien qu'un chemin refusé reçoit la même réponse, que le fichier existe ou non.
Le fichier est lu une seule fois, au démarrage : les modifications prennent effet au démarrage suivant. Sans le fichier, rien ne change. Le fichier lui-même n'est jamais servi. Comme tout segment de chemin commençant par . autre qu'un /.well-known/ initial, il est refusé avant le routage par le blocage des chemins commençant par un point.
Syntaxe
Tout ce que .gitignore prend en charge, .oxphpdeny le prend en charge :
- Les lignes vides et celles qui commencent par
#sont ignorées. Une marque d'ordre des octets (BOM) UTF-8 et les fins de ligne CRLF sont acceptées. - Les espaces finales sont supprimées, sauf si elles sont échappées (
\). !réinclut ce qu'une règle précédente a exclu.\#et\!en début de ligne sont un#et un!littéraux.- Un
/final restreint la règle aux répertoires. - Un
/au début ou au milieu ancre le motif au sommet deDOCUMENT_ROOT. Sans lui, le motif correspond à n'importe quelle profondeur :vendorcouvre/vendoret/lib/vendor. *et?restent dans un seul segment de chemin ;[abc],[a-z],[!0-9]et[^0-9]sont des classes de caractères. Comme dans git,?et une classe correspondent à un seul octet, et une classe ne correspond jamais à/:[!-]désigne n'importe quel octet d'un nom sauf-. Une lettre hors ASCII occupe deux octets ou plus, si bien que seul*peut la représenter : écrivez/caf*.txt, pas/caf?.txt. Les classes POSIX, les échappements par barre oblique inverse, les caractères hors ASCII, un-juste après une plage et une plage à l'envers ne sont pas pris en charge dans une classe, et une classe négative dans une règle de refus ou une règle>ne peut pas nommer une lettre ASCII (voir Différences avec.gitignore).**/au début,/**à la fin et/**/au milieu traversent les répertoires ;**partout ailleurs est un simple*. Une suite plus longue de*compte comme**, comme dans git :***/secret.txtcouvre/secret.txtà n'importe quelle profondeur.- La dernière règle qui correspond l'emporte, et rien sous un répertoire exclu ne peut être réinclus : avec
private/exclu,!private/ok.txtn'a aucun effet. Pour exclure le contenu d'un répertoire tout en en gardant une partie, excluez/private/*et réincluez!/private/ok.txt.
Règles > : confier la requête à l'application
> /storage/invoices/
> *.mapUne requête qui correspond à une règle > n'est pas servie depuis le disque. Elle part vers le script d'entrée avec REQUEST_URI intact, exactement comme une requête pour un fichier absent, et l'application peut vérifier une session et diffuser le fichier elle-même. Le script d'entrée est le contrôleur frontal (ENTRY_FILE=index.php) ou le worker. Les espaces et tabulations entre > et le motif sont autorisés ; \> est un > littéral. Une règle > ne peut pas être niée (!>x est une erreur), et un > seul est une erreur.
Une règle de refus s'applique toujours sous un répertoire > : avec > /storage/invoices/ et *.sql, /storage/invoices/dump.sql est refusé, quelle que soit la ligne qui vient en premier. Une règle ! qui lève le refus laisse le fichier au répertoire >, comme dans git : ajoutez !/storage/invoices/dump.sql après *.sql et le script d'entrée reçoit cette requête. ! ne réinclut rien sous un répertoire >, pas plus que sous n'importe quel répertoire exclu, si bien que le fichier n'est toujours pas servi depuis le disque.
Les règles > nécessitent un script d'entrée. En modes Traditionnel et SPA, elles font échouer le démarrage.
Différences avec .gitignore
- Une ligne mal formée fait échouer le démarrage, avec le chemin du fichier et le numéro de ligne, tout comme un fichier qui n'est pas en UTF-8. git ignore silencieusement de telles lignes.
{a,b}est littéral. Les accolades ne correspondent qu'à elles-mêmes, comme dans git.- Une classe qui nomme
/fait échouer le démarrage, que ce soit directement ([-/]) ou par une plage qui l'enjambe, comme[+-0]. git l'accepte et ne lui fait jamais correspondre/; écrivez le séparateur hors de la classe. - Une classe POSIX, une barre oblique inverse dans une classe ou un
-juste après une plage fait échouer le démarrage :[[:digit:]],[\]],[a-b-c]. git interprète chacun à sa façon. Écrivez plutôt la plage ([0-9]), placez]en premier pour le faire correspondre, et placez un-littéral en premier ou en dernier ([]-],[-a-c]). - Une plage à l'envers fait échouer le démarrage, comme
[z-a]ou[a--]. git la lit comme son seul premier caractère, si bien quex[z-a]ne couvre quexz; écrivez ce caractère, ou placez la borne inférieure en premier. - Une classe contenant un caractère hors ASCII fait échouer le démarrage, par exemple
[äÄ]. git compare une classe à un seul octet, comme le serveur, et un tel caractère en occupe deux ou plus :/[äÄ]dminne couvrirait ni/ädminni/Ädmin, et/[äÄ]*couvrirait/öffnungszeiten, qui commence par le même octet. Mettez*à sa place. - Un
/échappé fait échouer le démarrage (**\/secret.txt,\/vendor). git lit**\/secret.txtcommesecret.txtsous au moins un répertoire (pas/secret.txtlui-même), ce que le serveur ne ferait pas, et\/vendorne correspond à aucun chemin. Un séparateur n'a jamais besoin d'être échappé : écrivez/. - Une ligne qui couvre autre chose que ce qu'elle semble dire fait échouer le démarrage, c'est-à-dire une ligne qui commence par une espace ou une tabulation, se termine par une tabulation non échappée, contient
//à un endroit quelconque ou contient un octet NUL. git accepte chacune d'elles. Les blancs à l'une ou l'autre extrémité font partie du nom :> /storage/invoices/, indentée, est une règle de refus pour un chemin commençant par une espace, et non une règle>. Échappez-les avec\lorsqu'un nom commence ou se termine réellement par l'un d'eux.//vendoretvendor//ne correspondent à aucun chemin. git arrête un motif à un NUL, si bien quevendor, suivi d'un NUL et d'autre texte, couvre/vendordans git, là où le serveur lirait la ligne entière et ne couvrirait qu'un nom contenant ce NUL. Un fichier enregistré en UTF-16 contient un NUL après chaque caractère ASCII : enregistrez-le en UTF-8. - Une ligne faite d'espaces et de tabulations est vide. git lit une ligne contenant une tabulation comme un motif, pour un nom composé de ces blancs.
- Les règles de refus et les règles
>ignorent la casse des lettres ASCII ; les règles!non. Le serveur exécute/uploads/shell.PHPcomme un script PHP sur n'importe quel système de fichiers, et un système de fichiers insensible à la casse (macOS, Windows) sert/VENDOR/autoload.phpdepuisvendor/, si bien qu'une règle qui ne correspondrait qu'à une seule graphie laisserait passer les autres :*.phpcouvreshell.PHP, etvendor/couvre/Vendor/. Une règle!ne réinclut que la graphie qu'elle nomme (avec*et!/index.php,/INDEX.PHPreste refusé), si bien qu'ignorer la casse ne permet jamais au serveur de servir depuis le disque un chemin que la correspondance exacte lui aurait interdit. Pour qu'il en reste ainsi, une classe négative dans une règle de refus ou une règle>ne peut pas nommer une lettre ASCII ; si elle en nomme une, le démarrage échoue : en ignorant la casse,[!i]exclurait aussiI, et/[!i]*.phplaisserait exécuter/Info.php. Faites correspondre n'importe quel caractère à cet endroit et réincluez l'exception, qu'une règle!fait alors correspondre exactement :/*.phpet!/i*.php. git compare en respectant la casse, sauf sicore.ignorecaseest défini. - Quelques lettres hors ASCII sont lues comme de l'ASCII. Un système de fichiers insensible à la casse (macOS) les lit ainsi et sert
license.txtpour/licenſe.txt:ſ(s long) commes,ßetẞcommess, le symbole kelvin (U+212A) commek, les ligaturesfffiflffifflſtstcomme les lettres qu'elles lient, et le point d'interrogation grec (U+037E) et la varia (U+1FEF) comme;et`. Les règles de refus et les règles>les lisent ainsi elles aussi, dans la requête comme dans la règle :/license.txtcouvre/licenſe.txt, et/straße/couvre/strasse/. Une règle couvre toujours ce qu'elle couvrait telle qu'écrite, et les règles!ne lisent ces lettres que telles qu'écrites, si bien qu'un refus ne peut que s'élargir. - Les autres lettres hors ASCII ne correspondent que telles qu'écrites, et il en va de même pour chaque forme Unicode d'un nom. Une règle pour
/ädminne couvre pas/Ädmin, et une règle pourcafé.txtécrite avec unéprécomposé ne couvre pascafesuivi d'un accent combinant (U+0301). La plupart des systèmes de fichiers Linux font de chaque graphie un fichier distinct ; la question se pose donc sous macOS, qui traite chacune des deux paires comme un seul fichier, y compris à travers un montage bind de macOS vers Docker Desktop. Là, une requête pour l'autre graphie est servie depuis le disque. Mettez*à la place de ces lettres (/caf*.txt), puisque?et une classe correspondent à un seul octet, ou gardez ces fichiers hors deDOCUMENT_ROOT. - Au plus 256 règles, chacune d'au plus 128 octets. Une règle qui dépasse l'une ou l'autre limite fait échouer le démarrage avec son numéro de ligne (voir Limites).
- Un seul fichier, au sommet de
DOCUMENT_ROOT, lu au démarrage. Les fichiers des sous-répertoires ne sont pas lus. Ce doit être un fichier ordinaire d'au plus 1 MiB ; un lien symbolique vers un tel fichier convient. Un FIFO ou un périphérique fait échouer le démarrage sans être lu, et un fichier plus gros le fait échouer une fois son premier MiB lu.
Ce qui est comparé
Les règles sont comparées au chemin de la requête après le décodage des pourcentages et la normalisation. Les segments vides sont supprimés, si bien que //vendor/x, /vend%6Fr/x et /vendor%2Fx deviennent tous /vendor/x. Un segment commençant par . (., .., .git) n'atteint jamais les règles : la requête est refusée d'emblée avec un simple 404, sans PHP_DENY_FALLBACK et sans être comptée. L'exception est un /.well-known/ initial (RFC 8615), que le blocage des chemins commençant par un point laisse passer et que les règles voient comme n'importe quel autre chemin.
Une requête est une requête de répertoire lorsque son chemin décodé se termine par / (/admin/, /admin%2F, /admin//). Une règle avec un / final ne correspond qu'à ces requêtes, et à tout ce qui se trouve en dessous :
| Règle | /admin |
/admin/ |
/admin/x |
/admin/x/y |
|---|---|---|---|---|
admin/ |
— | refusé | refusé | refusé |
admin ou /admin |
refusé | refusé | refusé | refusé |
admin/*, admin/x |
— | — | refusé | refusé |
Le / seul ne correspond jamais, si bien qu'un fichier en liste d'autorisation sert toujours la racine du site :
*
!/index.php
!/assets/
!/assets/**
!/.well-known/
!/.well-known/**Conservez les lignes /.well-known/ si le site renouvelle son certificat TLS via ACME HTTP-01. Sans elles, /.well-known/acme-challenge/<token> est refusé.
Deux autres lectures d'une requête sont également jugées, et un refus selon l'une ou l'autre est maintenu :
- Une requête de répertoire est aussi jugée comme le fichier qu'elle nomme. Le routeur supprime la barre oblique finale et peut servir
/secret.txt/comme le fichiersecret.txt. - En mode Traditionnel, un segment
.phpsuivi d'une suite de chemin est jugé comme le script qu'il nomme./secret.php/x.jsexécutesecret.phpavec le PATH_INFO/x.js: il est donc refusé partout où/secret.phpl'est.
Dans les deux cas, !*/, la manière gitignore de réinclure tous les répertoires, ne peut pas faire revenir un fichier refusé. Dans une liste d'autorisation, réincluez un répertoire sans la barre oblique (!/docs) pour que /docs/ lui-même reste accessible. Aucun autre fichier vers lequel le routeur résout une requête n'est jugé, comme le montre la section suivante.
Le chemin de la requête, pas le fichier
Les règles voient le chemin demandé par le client, pas le fichier vers lequel le routeur le résout. Une requête qui se résout en un autre fichier est jugée sur son propre chemin :
/adminavec la règleadmin/: en mode Traditionnel, le routeur répond à/adminavecadmin/index.php, et/adminn'est pas une requête de répertoire./uploads/avec la règleuploads/*.php: l'index de répertoireuploads/index.phps'exécute, car le chemin de la requête est/uploads/./x.php/y.png(PATH_INFO) exécutex.phpbien que le chemin se termine par.png. Le script est nommé dans le chemin, si bien que le mode Traditionnel juge aussi la requête comme/x.php, comme décrit plus haut.
Pour fermer un répertoire, écrivez-le sans barre oblique finale (/admin). Pour empêcher l'exécution de .php dans les répertoires accessibles en écriture, utilisez aussi PHP_DENY_PATHS : il vérifie également le script vers lequel une requête se résout.
Modes de routage
| Mode de routage | Règles de refus | Règles > |
|---|---|---|
Traditionnel (sans ENTRY_FILE) |
Oui | Erreur de démarrage : il n'y a pas de script d'entrée |
SPA (ENTRY_FILE=index.html) |
Oui | Erreur de démarrage, car index.html ne peut pas prendre en charge une requête |
Framework (ENTRY_FILE=index.php) |
Oui | Contrôleur frontal |
Worker (WORKER_MODE_ENABLED=true) |
Oui, avec un repli par code de statut | Point d'entrée du worker |
Repli
Une règle de refus répond avec PHP_DENY_FALLBACK, le réglage qu'utilise PHP_DENY_PATHS : un statut HTTP (404 par défaut ; se combine avec ERROR_PAGES_DIR) ou un script PHP à l'intérieur de DOCUMENT_ROOT. Le script reçoit OXPHP_DENIED_PATH (le chemin de la requête, avec un / initial) et OXPHP_DENIED_PATTERN (la règle telle qu'écrite dans le fichier, par exemple /composer.*).
Le script peut se trouver sous un chemin que le fichier refuse. /_security/ le soustrait aux requêtes directes, et c'est le script lui-même qui répond à une requête directe le visant, comme à un refus. PHP_DENY_FALLBACK n'est lu que si le fichier contient au moins une règle de refus (ou si PHP_DENY_PATHS est en vigueur, en modes Traditionnel et SPA).
En mode Worker, le repli doit être un code de statut : un thread worker n'exécute rien d'autre que le point d'entrée du worker.
Lorsque .oxphpdeny et PHP_DENY_PATHS correspondent tous deux à une requête, c'est .oxphpdeny qui y répond.
Observabilité
- La métrique
oxphp_path_deny_totalcompte les requêtes refusées par une règle. Les règles>relèvent du routage, pas du refus, et ne sont pas comptées.oxphp_php_deny_totalcontinue de ne compter quePHP_DENY_PATHS; une requête refusée par les deux est comptée une seule fois, dansoxphp_path_deny_total. - Sur
/config, l'entréedeny_filedonne le nombre de règles par action et le type de repli, jamais les règles elles-mêmes (voir Serveur interne). - Le journal de démarrage indique
loaded 5 rules (3 deny, 1 entry, 1 allow) from /var/www/html/public/.oxphpdeny. AvecLOG_LEVEL=debug, une ligneoxphpdeny rulepar règle montre comment elle a été lue : numéro de ligne, action, motif tel qu'écrit et glob vers lequel il a été compilé. - Pour chaque requête, chaque refus journalise
request denied by .oxphpdenyau niveaudebug, avec le chemin, la règle et sa ligne. Une requête refusée pour sa profondeur ou sa longueur journaliserequest refused: path too deep or too long to match against .oxphpdenyau niveaudebug, avec son nombre de segments et sa longueur ; elle n'est pas comptée.
Erreurs de démarrage
| Condition | Résultat |
|---|---|
Pas de .oxphpdeny |
Fonctionnalité désactivée ; rien ne change |
| Fichier illisible ou non UTF-8, ou lien symbolique dont la cible est absente | Erreur de démarrage |
| Ligne mal formée | Erreur de démarrage avec <path>: line N: <message> |
| Plus de 256 règles, ou un motif de plus de 128 octets | Erreur de démarrage avec le numéro de ligne |
Règle > en mode Traditionnel ou SPA |
Erreur de démarrage |
Mode Worker, une règle de refus et un script comme PHP_DENY_FALLBACK |
Erreur de démarrage |
PHP_DENY_FALLBACK invalide et au moins une règle de refus |
Erreur de démarrage |
Fichier vide, uniquement des commentaires ou uniquement des règles ! |
Rien à refuser : le routage est inchangé, limite de profondeur comprise ; /config montre le fichier comme chargé, avec ses nombres de règles |
oxphp config --check signale les erreurs du fichier avant un déploiement (config: INVALID). Les vérifications du repli s'exécutent lorsque le serveur construit son routage, au démarrage.
Limites
- La profondeur et la longueur du chemin sont plafonnées. Avec au moins une règle de refus ou une règle
>, un chemin de requête de plus de 64 segments de profondeur, ou dont le nombre de segments multiplié par la longueur dépasse 128 KiB, reçoit un404sans être comparé. La correspondance revérifie chaque répertoire parent et parcourt le chemin de chacun, si bien que son coût croîtrait sinon avec les deux. Un chemin d'au plus 2 KiB n'est refusé que pour sa profondeur ; au-delà, le budget est épuisé dès 16 segments à 8 KiB, ou 3 à environ 43 KiB. - Un fichier contient au plus 256 règles, chaque motif faisant au plus 128 octets, sans compter un
!ou un>initial. Un chemin de requête est comparé aux règles une par une, et à chaque répertoire parent aussi, si bien que ces limites et celle ci-dessus bornent le coût d'une requête. Un fichier typique prend bien moins d'une milliseconde par requête. Un fichier aux deux limites, conçu pour être lent (256 longues règles commençant par/x/**/*, face à des chemins construits pour leur correspondre presque), prend environ 40 ms par requête et conserve environ 50 MB d'état de correspondance par thread worker Tokio. - Pour la sensibilité à la casse, voir Différences avec
.gitignore. - Il n'y a pas de rechargement. Redémarrez pour appliquer une modification.
Exemples
Une application héritée servie depuis la racine de son projet :
/vendor
/composer.*
/config
*.sql
*.sql.gz
*.bak
*.log/readme.html
/license.txt
/wp-config-sample.php
/wp-content/debug.logEn mode Framework, le contrôleur frontal reçoit /storage/invoices/2026-07.pdf, vérifie la session et diffuse le fichier :
> /storage/invoices/Prompts pour votre agent
Ces prompts supposent un agent connecté au serveur MCP de la documentation OxPHP — il lira lui-même les pages dont il a besoin.
Écris un fichier `.oxphpdeny` pour l'application de ce dépôt. Lis la page `security/oxphpdeny` via le serveur MCP de la documentation OxPHP.
Détermine quel répertoire est `DOCUMENT_ROOT` et quel mode de routage utilise le déploiement, puis trouve ce qui, sous ce répertoire, ne devrait jamais être servi — `vendor/`, `composer.*`, fichiers de configuration, dumps de base de données, sauvegardes, journaux, source maps — et lesquels de ces chemins l'application sert elle-même et ont plutôt besoin d'une règle `>`. Donne-moi le fichier avec un commentaire sur chaque règle, et liste ce qu'il ne peut pas couvrir parce que les règles comparent le chemin de la requête et non le fichier vers lequel il se résout.Porte vers `.oxphpdeny` les règles de refus de mon ancienne configuration de serveur web. Lis la page `security/oxphpdeny` via le serveur MCP de la documentation OxPHP.
Trouve dans ce dépôt les blocs `location` nginx qui font `deny all` ou `return 403`/`404`, ainsi que les règles `.htaccess` qui refusent l'accès, et traduis chacun en une ligne `.oxphpdeny`. Dis-moi lesquels le blocage des chemins commençant par un point couvre déjà et lesquels le fichier ne peut pas exprimer.Passe en revue le fichier `.oxphpdeny` de ce dépôt à la recherche de failles. Lis la page `security/oxphpdeny` via le serveur MCP de la documentation OxPHP.
Confronte chaque règle à la façon dont cette application est déployée : répertoires écrits avec une barre oblique finale qu'un index de répertoire atteint encore, noms contenant des lettres hors ASCII que macOS sert sous une autre graphie, répertoires accessibles en écriture qui ont aussi besoin de `PHP_DENY_PATHS`, et liste d'autorisation qui bloquerait `/.well-known/acme-challenge/`. Donne-moi le fichier corrigé et dis ce que ferme chaque modification.Voir aussi
- Liste de refus d'exécution PHP —
PHP_DENY_PATHSetPHP_DENY_FALLBACK - Blocage des chemins commençant par un point — fichiers et répertoires cachés, refusés avant l'exécution de ces règles
- Routage — modes de routage et sécurité des chemins
- Métriques —
oxphp_path_deny_total