Disassembler

Artificial intelligence is no match for natural stupidity.
30říjen2014

Neprůstřelný URL rewrite


SEO-friendly URL, Pretty URL, čistá URL, sémantická URL. Každý tomu říká jinak, ale účel je pořád stejný. Místo nečitelných oblud typu /index.php?page=article&id=115 uživatelům nebo aplikacím nabídnout něco smysluplného jako /clanek/neprustrelny-url-rewrite. K tomuto účelu se na Apache HTTP serveru nejčastěji používá mod_rewrite. Jsa middleware administrátor lehce posedlý bezpečností, překvapilo mne, kolik taková standardně implementovaná sada rewrite pravidel může prozradit potenciálně zneužitelných informací.

Kudy tudy cestička


Před pár dny jsem implementoval nějaké nové funkce do svého pet projectu, kterému říkám Mail Admin. Jedná se o webové rozhraní pro správu mailových schránek, aliasů, automatických odpovědí a dalších mailových záležitostí, psaný v PHP. A když už jsem měl ten projekt otevřený, říkal jsem si, že bych si mohl udělat nějaký bezpečnostní miniaudit. Celá aplikace je postavená nad velice jednoduchým MVC modelem (pouze modelem, PHP frameworky nepoužívám) s RESTful-like URL, aby v případě, že by někomu hráblo a chtěl k tomu vytvořit mobilní aplikaci, byla veškerá práce s backendem již hotová. V praxi to znamená, že requesty pak vypadají následovně:

GET  /domains           – vypíše seznam domén 
GET  /example.com/boxes – vypíše schránky v doméně example.com (má-li uživatel oprávnění)
POST /add-box           – přidá schránku dle parametrů v POST
POST /delete-alias      – smaže alias dle parametrů v POST

A tak dále. Aby tohle mohlo fungovat, v kořenovém adresáři aplikace musí sedět .htaccess a pomocí rewrite pravidel posílat všechny požadavky PHP skriptu, kde je nějaký můj směrovač požadavků přečte, vyhodnotí a nechá aplikaci provést příslušnou operaci. Samozřejmě nedává smysl, aby rewrite chytal všechny požadavky, protože pak by musel nějakým způsobem routovat i obrázky, kaskádové styly, javascripty a další statický obsah, což by vytvořilo naprosto zbytečnou režii jak pro programátora, tak i pro server.

RewriteEngine On


Podotýkám, že funguju na Apache 2.4, což bude důležité zejména pro kouzla a čáry v jednom z posledních odstavců, protože budu přidávat parametr, který Apache 2.2 nezná. Pro začátek tedy řekněme, že moje aplikace vypadá v souborovém systému následovně:

/
├─ css/
│  ├─ style.css
│  └─ jquery-ui.css
├─ img/
│  ├─ add.png
│  ├─ delete.png
│  └─ edit.png
├─ js/
│  ├─ script.js
│  ├─ jquery.js
│  └─ jquery-ui.js
├─ .htaccess
├─ actions.php
├─ config.php
├─ database.php
├─ index.php
└─ view.php

Z čehož vyplývá, že bych neměl chytat a přepisovat požadavky směřující do adresářů css, image a js. Naopak bych asi nechtěl, aby mi nějaký vyčůránek zázračil s *.php skripty, takže u těch by rewrite byl žádoucí. Naivní .htaccess pro danou situaci by tedy mohl vypadat třeba takto:

RewriteEngine On

RewriteCond $1 ^(css|img|js)
RewriteRule ^(.*)$ - [L]

RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]

Přeloženo do lidské řeči: „Požadavky začínající řetězci css, img nebo js nepřepisuj a nech projít v nezměněné podobě, všechny ostatní požadavky přepiš jako index.php?q=puvodni_pozadavek.“ V téhle fázi většina PHP patlalů skončí a je šťastná, že jim aplikace funguje. Já už jsem ale takovej, že prostě musím do všeho rejpat.

Gotta catch’em all


Tak v prvé řadě se mi nelíbí, že rewrite pustí všechny requesty, které prostě začínají daným řetězcem. Pro

GET /css/style.css

je to v pořádku, ale pro

GET /css
GET /css/
GET /css/kravina.css

už to v pořádku není. Potenciální útočník tak může na server pustit nějakého pavouka a nachytat si tak všechny cesty, které jsou takovýmto rewritem nevhodně ošetřeny. Zjištění názvu adresáře samozřejmě samo o sobě není nějakou vysoce nebezpečnou zranitelností, ale jistý způsob vyzrazení informací to již je. Rád bych tedy, aby můj RewriteCond chytal pouze požadavky, na které skutečně může vrátit soubor existující v souborovém systému a v případě, že takový soubor neexistuje, provedl rewrite na index.php?q=něco, stejně jako to dělá ve všech ostatních případech. To lze naštěstí provést celkem jednoduše, protože direktiva RewriteCond umí přijmout parametr -f místo jména souboru a místo porovnání názvu pak kontroluje, zda soubor existuje v souborovém systému. Analogicky umí udělat totéž s -d pro adresáře a -l pro symlinky. Podmínku přepisu tedy upravím následovně:

RewriteEngine On

RewriteCond $1 ^(css|img|js)/.+
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^(.*)$ - [L]

RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]

Řetězec, kterým porovnávám první RewriteCond jsem rozšířil o nějaký rozsypaný čaj. Tím říkám, že pro následující rewrite mě zajímají pouze soubory uvnitř oněch tří adresářů a tedy requesty

GET /css
GET /css/

do něj už nespadnou a jdou rovnou na další rewrite skupinu, která už se na nic neptá a přepisuje na index.php?q=css, resp. index.php?q=css/. Druhý RewriteCond se mi pak postará i o zbývající nežádoucí

GET /css/kravina.css

Protože pravidlo v tomto řádku propouští pouze existující soubory, kterým kravina.css není, je vykonávání této rewrite skupiny u konce a může se vesele pokračovat na další, která požadavek opět přepíše na index.php?q=css/kravina.css.

Zdivočelé redirecty


V této fázi mám tedy ošetřeny všechny možné kombinace URL a neměla by existovat situace, ve které bych dostal nejznámější HTTP status 404 Not Found bez toho, aniž bych si jej sám nenastavil v aplikaci. Apache buď uživateli odevzdá existující statický soubor, nebo jeho požadavek přesměruje PHP skriptu, který už se na základě aplikační logiky rozhodne, co se bude dít dál.

Zajímavá situace ovšem nastává, je-li na serveru zapnutý mod_dir, který je, vzhledem k tomu, že se jedná o modul zajišťující i implicitní vyhledávání index.html, index.php a dalších souborů uvedených v direktivě DirectoryIndex, zapnutý na drtivé většině serverů. Tento modul se mimo vyhledávání indexových souborů stará také o přepisování koncových lomítek (trailing slash) u adresářů. Jednoduše řečeno, když si zažádáte o GET /adresar automaticky vám požadavek přepíše na GET /adresar/ a pošle vám informaci o tomto přepisu jako HTTP 301 Moved Permanently. Pak se ještě podívá, jestli náhodou neexistuje nějaký /adresar/index.html, /adresar/index.php nebo něco podobného a v případě, že existuje, naservíruje vám jeho obsah. Tentokrát už formou plnohodnotného rewrite bez jakýchkoliv redirectů. Jak je z tohoto chování možno vidět, dochází tu k přepisování, aniž by byl mod_rewrite vůbec aktivní. Pokud ovšem aktivní je, začnou se mezi sebou bít a dojde k tomu, že k automatickému přidání koncového lomítka se ještě přimotá kus RewriteRule, takže klient na request

GET /css HTTP/1.1
Host: www.example.com

Dostane odpověď

HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/css/?q=css

Což je jednak kravina, a jednak únik informace jako vyšitý, protože tamto ?q= je vytaženo z .htaccess a v ideálním světě se rozhodně obsah .htaccess nemá dostat k uživateli, pokud to není explicitně vyžádáno posláním redirectu. A teď to nejlepší – tohle chování bylo poprvé oficiálně popsáno 14. září 2004 ve verzi Apache 2.0.50 v bugzilla tiketu 31210, který byl o dva roky později sloučen s tiketem 40373, kde je totéž chování popsáno trochu jednodušším způsobem. Jestli open source komunita opravuje všechny bugy s patnácti až dvacetiletým zpožděním, není divu, že lidi stále dávají přednost proprietárním produktům.

Workaround


Tohle evidentně špatné chování se dá obejít vytvořením rovnáku na vohejbák. V mod_dir existuje direktiva DirectorySlash, která zapíná a vypíná ono problematické přilepování koncových lomítek. Dá se nastavit i v .htaccessa spadá do Override skupiny Indexes, jejíž úprava naštěstí obvykle bývá na serverech povolena. Ovšem pozor, přidáte-li do vašeho .htaccess pouze DirectorySlash Off, se zlou se potážete, protože vám přestane fungovat doplňování koncového lomítka i pro kořenový adresář aplikace, ve kterém se .htaccess nachází. A jako na potvoru tohle lomítko zrovna potřebujete, protože jinak nebude přepsán prázdný požadavek a vyhledání souboru index.php se vůbec neprovede, takže v lepším případě bude na takový požadavek odpovědí 403 Forbidden, v horším případě pak výpis adresáře, což potenciálního útočníka jistě potěší. DirectorySlash je tedy náš vohejbák. Rovnákem na něj je pak speciální hodnota direktivy RewriteOptions, kterou Apache 2.4 zavedl právě pro řešení tohoto problému. Výtah z dokumentace:

AllowNoSlash
    By default, mod_rewrite will ignore URLs that map to a directory on disk but lack
    a trailing slash, in the expectation that the mod_dir module will issue the client
    with a redirect to the canonical URL with a trailing slash.

    When the DirectorySlash directive is set to off, the AllowNoSlash option can
    be enabled to ensure that rewrite rules are no longer ignored. This option makes it
    possible to apply rewrite rules within .htaccess files that match the directory
    without a trailing slash, if so desired.

    Available in Apache HTTP Server 2.4.0 and later.

Takže šup s tím do našeho .htaccess.

DirectorySlash Off
RewriteEngine On
RewriteOptions AllowNoSlash

RewriteCond $1 ^(css|img|js)/.+
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^(.*)$ - [L]

RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]

Teď už konečně k žádným rewrite leakům ani jinému vyzrazení informací nedochází a aplikace funguje tak jak bylo zamýšleno. Dvouhodinový maraton pokusů a omylu doprovázených vydatným nadáváním a virtuálním listováním v dokumentaci tím pro mě končí a můžu si udělat pomyslnou čárku za další den, kdy jsem byl překvapen něčím, o čem jsem předpokládal, že mě už nikdy nepřekvapí.