.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.

Avatar
Georg 07.02.2017 16:48
So ganz nebenbei erhöht das explizite Abschalten des Parsens und Suchens die Apache Performance durchaus nicht unerheblich. Vor allem, wenn man eine nicht unerhebliche Menge an parallelen DocRoots hat...
Avatar
Jens 03.02.2016 01:34
Das ist ja ungeheuerlich. Man denke nur an die (frühere) Konfiguration von ~/public_html... Das erklärt dann auch merkwürdiges Verhalten, wenn eine ~/.htaccess (ausserhalb des Document-Roots) vorhanden war. Auch erklärt das die in der Standardkonfiguration enthaltene Direktive "<Directory />". Ich habe mich schon lange gewundert, wozu die überhaupt gut sein soll, da das ja auch ausserhalb des Document-Roots liegt. Danke für die Info, obwohl ich nicht mehr betroffen bin, da hier nur noch ein anderer httpd zum Einsatz kommt ;)

2 Kommentare

Post a comment

Verwandter Inhalt