.htaccess außerhalb des DocumentRoots
Der Apache HTTPD ist ein schnuckeliger Webserver, den ich gerne einsetze. Und manchmal überrascht er mich. Erst kürzlich hat ein Kunde in einem Unterverzeichnis seiner Webpräsenz ein neue Webseite aufgebaut, die als einer virtueller Host unter eigenem Namen erscheinen sollte. Aber das ging spektakulär schief.
Des Kunden Wille …
Ausgangspunkt war ein Kunde mit einer Webseite im shared Hosting bei uns. Er hat ein Directory, auf das ein vHost mit einem extra Servernamen zeigt.
<VirtualHost *:80> ServerName www.do.main ServerAlias do.main ServerAlias kunde.do.main DocumentRoot /www/kunde/ </VirtualHost>
So weit, so einfach.
Eines schönen Tages hat der Kunde beschlossen eine neue Webseite zu beginnen. Er hat dafür in einem Unterverzeichnis projekt die notwendigen PHP-Scripte und Dateien angelegt und konnte dann direkt mit http://www.do.main/projekt/ die Installation testen.
Als er damit fertig war, bat er um einen neuen Domainnamen projekt.do.main unter dem das Projekt laufen soll.
<VirtualHost *:80> ServerName projekt.do.main DocumentRoot /www/kunde/projekt/ </VirtualHost>
So weit, so einfach.
Da er schon von dem Projekt erzählt hat, setzt er in der /www/kunde/.htaccess ein Redirect auf die neue URL.
RewriteRule /projekt/(.*) http://projekt.do.main/$1
Nur funktioniert das zu allem Erstaunen nicht. Zugriffe landen auf der falschen Webseite.
- http://www.do.main/projekt/ wird an http://projekt.do.main/ weiter geleitet.
- http://projekt.do.main/ leitet an http://www.do.main/ weiter.
Ursachenforschung
Der Kunde hat in der /www/kunde/.htaccess noch weitere Regeln, insbesondere eine Regel, die den Servernamen kanonifiziert.
RewriteCond %{HTTP_HOST} !^www.do.main [NC] RewriteRule ^(.*)$ http://www.do.main/$1
Ganz offensichtlich greift diese Regel auch innerhalb der neuen vHosts. Dabei liegt sie außerhalb der DocumentRoot.
Es nützt auch nichts, eine eigene /www/kunde/projekt/.htaccess anzugeben, die Datei im darüber liegenden Verzeichnis wird trotzdem beachtet.
Treiben wir es auf die Spitze. Wie weit hoch kann ich eine .htaccess schieben?
# echo "Redirect /test http://httpd.apache.org" > /.htaccess
Und das ergibt einen Fehler 500: Internal Server Error. Im Log steht:
[core:notice] [pid 14104] [client 127.0.0.1:44529] /.htaccess: Redirect not allowed here
Ganz offensichtlich geht der Webserver alle Directories bis zur Wurzel durch, um eine .htaccess Datei zu finden. Und wenn er eine findet, dann darf er die Kommandos darin nicht ausführen, was den Fehler verursacht.
In der Doku steht:
Default: AllowOverride None When this directive is set to None and AllowOverrideList is set to None .htaccess, files are completely ignored. In this case, the server will not even attempt to read .htaccess files in the filesystem.
Da der Default None ist und in der Serverkonfiguration erst ab den Kundenwebseiten AllowOverride gesetzt wird, sollte die Datei doch ignoriert werden!
Ganz offensichtlich ist das nicht der Fall.
Default ist nicht Default
Setzt man probeweise in der Webserver-Konfiguration AllowOverride explizit
<Directory /> AllowOverride None </Directory>
so ist der Fehler weg, die /.htaccess wird ignoriert.
Dies ist aber der Defaultwert laut Dokumentation! Warum sollte man den Defaultwert explizit setzen müssen um dann ein abweichendes Verhalten zu bekommen?
Ein Blick in den Source zeigt, dass es eine directory-spezifische Variable override gibt, die den Zugriff auf .htaccess unterbindet.
ap_conf_vector_t *htaccess_conf = NULL; /* No htaccess in an incomplete root path, * nor if it's disabled */ if (seg < startseg || (!opts.override && opts.override_list == NULL)) { break; }
Nur wenn die Variable override den Wert 0 hat, wird die Verarbeitung von .htaccess Dateien unterbunden.
Aber welchen Wert hat denn diese Variable standardmäßig? Also wenn man sie nicht explizit setzt?
Zuerst einmal kommentiere ich die Zeile aus.
<Directory /> # AllowOverride None </Directory>
Ergebnis: 500: Internal Server Error.
Also rein in den Code und nachgeschaut:
AP_INIT_RAW_ARGS("AllowOverride", set_override, NULL, ACCESS_CONF, "Controls what groups of directives can be configured by per-directory " "config files"),
Die Funktion, die diese Direktive parst, heißt also set_override. Na die ist schnell gefunden:
static const char *set_override(cmd_parms *cmd, void *d_, const char *l) { core_dir_config *d = d_; char *w; char *k, *v; const char *err; /* Throw a warning if we're in <Location> or <Files> */ if (ap_check_cmd_context(cmd, NOT_IN_LOCATION | NOT_IN_FILES)) { ap_log_error(APLOG_MARK, APLOG_WARNING, 0, cmd->server, APLOGNO(00114) "Useless use of AllowOverride in line %d of %s.", cmd->directive->line_num, cmd->directive->filename); } if ((err = ap_check_cmd_context(cmd, NOT_IN_HTACCESS)) != NULL) return err; d->override = OR_NONE; while (l[0]) { w = ap_getword_conf(cmd->temp_pool, &l);
Aha, der Defaultwert ist also OR_NONE, wenn die Direktive geparst wird.
#define OR_NONE 0 /**< *.conf is not available anywhere in this override */
Da kann aber nichts passieren. Ich bin also auf der falschen Spur.
Offenbar ist der Defaultwert ein Wert, der vor dem Parsen schon da ist.
static void *create_core_dir_config(apr_pool_t *a, char *dir) { core_dir_config *conf; conf = (core_dir_config *)apr_pcalloc(a, sizeof(core_dir_config)); /* conf->r and conf->d[_*] are initialized by dirsection() or left NULL */ conf->opts = dir ? OPT_UNSET : OPT_UNSET|OPT_SYM_LINKS; conf->opts_add = conf->opts_remove = OPT_NONE; conf->override = OR_UNSET|OR_NONE; conf->override_opts = OPT_UNSET | OPT_ALL | OPT_SYM_OWNER | OPT_MULTI;
Was steht da? Welcher Default wird da gesetzt?!
#define OR_UNSET 32 /**< bit to indicate that AllowOverride has not been set */
Bitte?
Verwendet wird der Wert dafür, um das Mergen von Konfigurationen abzubrechen. Er hat also nur interne Bedeutung.
Im Zusammenhang mit dem Test auf den Wert 0 ist dieser Default jedoch kontraproduktiv. Die Dokumentation ist fehlerhaft. Sie müsste lauten:
Default: unset Unless this directive is set explicitly, all commands are denied but all files are parsed.
Kann jetzt jemand mal einen Bugreport einkippen?
… ist sein Himmelreich
Was hat das alles mit dem Kunden zu tun?
Nicht viel. Er hat keine Chance, das Problem verschachtelter DocumentRoots zu lösen.
Eine Idee scheint zu sein, einen anderen Namen für die .htaccess per AccessFileName zu setzen. Aber wenn ich so in den Code schaue, wird wohl über alle diese Namen global iteriert.
/* loop through the access names and find the first one */ while (access_names[0]) { const char *access_name = ap_getword_conf(r->pool, &access_names);
Wer seinen Kunden ärgern will, setzt also AccessName auf _htaccess und index.php.
Die kanonische Lösung heißt natürlich:
- Trennung der DocumentRoots, so dass diese nebeneinander liegen.
- Verschiebung des existierenden Verzeichnisses an eine andere Stelle außerhalb der bestehenden DocumentRoot.
Damit geht's einfach.
Total 2 comments