Detect misconfigured hostnames during WCFSetup
authorTim Düsterhus <duesterhus@woltlab.com>
Thu, 6 Aug 2020 12:38:02 +0000 (14:38 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Thu, 6 Aug 2020 12:38:02 +0000 (14:38 +0200)
Misconfigured reverse reverse proxies might rewrite the `host` header to the
upstream's hostname, instead of preserving the `host` as it was sent by the
web browser. Such a misconfiguration will cause WoltLab Suite to generate
incorrect absolute URLs and more importantly this also causes it to specify
an incorrect `domain` within cookies. The latter leads to the browser ignoring
the cookie. At the end of WCFSetup this ultimately leads to the ACP session
cookie being ignored, which in turn leads to failing the transition from
WCFSetup into the package installation. Instead the user will be bounced to
the LoginForm which fails to load, because the necessary option.xml was not
yet installed.

An example HAProxy configuration that reproduces the issue is as follows:

    listen test
     mode http
     bind *:80
     http-request set-header host 172.19.0.5
     server nginx 172.19.0.5:80

If the WCFSetup is accepted via any hostname that is not `172.19.0.5`, e.g.
by using `localhost` then cookies will fail to stick within the web browser.

This commit extends the system requirements step to:

- Compare the HTTP_HOST as seen by the web server against both:
   1) The `Referer` header.
   2) The `window.location.host` value in JavaScript.
  If any of those mismatches, then the web server is not correctly configured.
- Read a cookie that was set earlier.
  If this cookie is missing, then most likely the `domain` property was
  incorrectly specified.

This commit most likely resolves #3024.

wcfsetup/install/files/lib/system/WCFSetup.class.php
wcfsetup/setup/lang/setup_de.xml
wcfsetup/setup/lang/setup_en.xml
wcfsetup/setup/template/stepShowSystemRequirements.tpl

index 9aa4bf6c007d972dbcf2704d0fe87402a40d322f..89ced00ae4b320b250df55cfd858aea49a39c307 100644 (file)
@@ -19,6 +19,7 @@ use wcf\system\io\File;
 use wcf\system\io\Tar;
 use wcf\system\language\LanguageFactory;
 use wcf\system\package\PackageArchive;
+use wcf\system\request\RouteHandler;
 use wcf\system\session\ACPSessionFactory;
 use wcf\system\session\SessionHandler;
 use wcf\system\setup\Installer;
@@ -225,6 +226,8 @@ class WCFSetup extends WCF {
                if (isset($_REQUEST['step'])) $step = $_REQUEST['step'];
                else $step = 'selectSetupLanguage';
                
+               header('set-cookie: wcfsetup_cookietest='.TMP_FILE_PREFIX.'; domain=' . str_replace(RouteHandler::getProtocol(), '', RouteHandler::getHost()) . (RouteHandler::secureConnection() ? '; secure' : ''));
+               
                // execute current step
                switch ($step) {
                        /** @noinspection PhpMissingBreakStatementInspection */
@@ -384,6 +387,16 @@ class WCFSetup extends WCF {
                // openssl extension
                $system['openssl']['result'] = @extension_loaded('openssl');
                
+               // misconfigured reverse proxy / cookies
+               $system['hostname']['result'] = true;
+               list($system['hostname']['value']) = explode(':', $_SERVER['HTTP_HOST'], 2);
+               if (!empty($_SERVER['HTTP_REFERER'])) {
+                       $refererHostname = parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST);
+                       $system['hostname']['result'] = $_SERVER['HTTP_HOST'] == $refererHostname;
+               }
+               
+               $system['cookie']['result'] = !empty($_COOKIE['wcfsetup_cookietest']) && $_COOKIE['wcfsetup_cookietest'] == TMP_FILE_PREFIX;
+               
                WCF::getTPL()->assign([
                        'system' => $system,
                        'nextStep' => 'configureDirectories'
index a68f251db71df5e472858669d650ad00e3faf10e..8e791cf315beff8f362242e2c47eddcadabbadf4 100644 (file)
                <item name="wcf.global.systemRequirements.memoryLimit.description"><![CDATA[Der PHP-Skripten zur Verfügung stehende Arbeitsspeicher ist für einen einwandfreien Betrieb der Software zu gering.]]></item>
                <item name="wcf.global.systemRequirements.openSSL"><![CDATA[„OpenSSL“ PHP-Erweiterung]]></item>
                <item name="wcf.global.systemRequirements.openSSL.description"><![CDATA[Ihre PHP-Version wurde ohne Unterstützung für OpenSSL kompiliert und kann daher keine sicheren Verbindungen aufbauen.]]></item>
+               <item name="wcf.global.systemRequirements.hostname"><![CDATA[Hostname]]></item>
+               <item name="wcf.global.systemRequirements.hostname.requirement"><![CDATA[Korrekter Hostname]]></item>
+               <item name="wcf.global.systemRequirements.hostname.description"><![CDATA[Der vom Webserver ermittelte Hostname muss dem Hostnamen in der Adressleiste des Webbrowsers entsprechen.]]></item>
+               <item name="wcf.global.systemRequirements.cookie"><![CDATA[Cookies]]></item>
+               <item name="wcf.global.systemRequirements.cookie.description"><![CDATA[Gesetze Cookies müssen vom Webbrowser zurück an den Server gesendet werden.]]></item>
                <item name="wcf.global.next"><![CDATA[Lade nächsten Schritt …]]></item>
                <item name="wcf.global.next.description"><![CDATA[Die Installation lädt im Moment den nächsten Schritt. Bitte haben Sie einen Augenblick Geduld.]]></item>
                <item name="wcf.global.languages"><![CDATA[Sprachen wählen]]></item>
index 40933e817af75e79236a1375e4fcc7eee63e0cc0..52bef8182aea70c0b500a60db95a0ab46eb22ec8 100644 (file)
                <item name="wcf.global.systemRequirements.memoryLimit.description"><![CDATA[The maximum available memory of PHP scripts is too low to properly run the software.]]></item>
                <item name="wcf.global.systemRequirements.openSSL"><![CDATA['OpenSSL' PHP extension]]></item>
                <item name="wcf.global.systemRequirements.openSSL.description"><![CDATA[Your PHP version has been compiled without OpenSSL support, it is required to establish secure connections.]]></item>
+               <item name="wcf.global.systemRequirements.hostname"><![CDATA[Host Name]]></item>
+               <item name="wcf.global.systemRequirements.hostname.requirement"><![CDATA[Correct Host Name]]></item>
+               <item name="wcf.global.systemRequirements.hostname.description"><![CDATA[The host name determined by the web server must match the host name within the web browser's URL bar.]]></item>
+               <item name="wcf.global.systemRequirements.cookie"><![CDATA[Cookies]]></item>
+               <item name="wcf.global.systemRequirements.cookie.description"><![CDATA[The web browser must send cookies back to the web server.]]></item>
                <item name="wcf.global.next"><![CDATA[Loading next step …]]></item>
                <item name="wcf.global.next.description"><![CDATA[The installation is currently loading the next step, please wait.]]></item>
                <item name="wcf.global.languages"><![CDATA[Choose languages to install]]></item>
index 4f8a8a112afa25526ba071232877953da3665296..34952f5db817e9e9a03f7dbca44f5d19b24e55fe 100644 (file)
                                </dl>
                        </div>
                </section>
+               
+               <section class="section">
+                       <h2 class="sectionTitle">{lang}wcf.global.systemRequirements.hostname{/lang}</h2>
+                       
+                       <div class="row rowColGap formGrid">
+                               <dl class="col-xs-12 col-md-6">
+                                       <dt>{lang}wcf.global.systemRequirements.element.required{/lang}</dt>
+                                       <dd>{lang}wcf.global.systemRequirements.hostname.requirement{/lang}</dd>
+                               </dl>
+                               
+                               <dl class="col-xs-12 col-md-6">
+                                       <dt>{lang}wcf.global.systemRequirements.element.yours{/lang}</dt>
+                                       <dd>
+                                               <span id="hostnameBadge" class="badge {if !$system.hostname.result}red{else}green{/if}">{$system.hostname.value}</span>
+                                               <small{if $system.hostname.result} style="display: none"{/if}>{lang}wcf.global.systemRequirements.hostname.description{/lang}</small>
+                                       </dd>
+                                       <script>
+                                               (function () {
+                                                       var badge = document.getElementById('hostnameBadge');
+                                                       if (badge.classList.contains('green')) {
+                                                               var serverHost = badge.textContent;
+                                                               var browserHost = window.location.host;
+                                                               if (serverHost != browserHost) {
+                                                                       badge.classList.remove('green');
+                                                                       badge.classList.add('red');
+                                                                       badge.nextElementSibling.style.display = '';
+                                                                       document.querySelector('.formSubmit input[type="submit"]').disabled = true;
+                                                               }
+                                                       }
+                                               })();
+                                       </script>
+                               </dl>
+                       </div>
+               </section>
+               
+               <section class="section">
+                       <h2 class="sectionTitle">{lang}wcf.global.systemRequirements.cookie{/lang}</h2>
+                       
+                       <div class="row rowColGap formGrid">
+                               <dl class="col-xs-12 col-md-6">
+                                       <dt>{lang}wcf.global.systemRequirements.element.required{/lang}</dt>
+                                       <dd>{lang}wcf.global.systemRequirements.active{/lang}</dd>
+                               </dl>
+                               
+                               <dl class="col-xs-12 col-md-6">
+                                       <dt>{lang}wcf.global.systemRequirements.element.yours{/lang}</dt>
+                                       <dd>
+                                               <span class="badge {if !$system.cookie.result}red{else}green{/if}">
+                                               {if !$system.cookie.result}{lang}wcf.global.systemRequirements.notActive{/lang}{else}
+                                                       {lang}wcf.global.systemRequirements.active{/lang}
+                                               {/if}</span>
+                                               {if !$system.cookie.result}<small>{lang}wcf.global.systemRequirements.cookie.description{/lang}</small>{/if}
+                                       </dd>
+                               </dl>
+                       </div>
+               </section>
        </section>
        
        <section class="section">
        </section>
 
        <div class="formSubmit">
-               <input type="submit" value="{lang}wcf.global.button.next{/lang}"{if !$system.phpVersion.result || !$system.sql.result || !$system.memoryLimit.result || !$system.graphicsLibrary.result} disabled{/if} accesskey="s">
+               <input type="submit" value="{lang}wcf.global.button.next{/lang}"{if !$system.phpVersion.result || !$system.sql.result || !$system.memoryLimit.result || !$system.graphicsLibrary.result || !$system.hostname.result || !$system.cookie.result} disabled{/if} accesskey="s">
                <input type="hidden" name="step" value="{@$nextStep}">
                <input type="hidden" name="tmpFilePrefix" value="{@$tmpFilePrefix}">
                <input type="hidden" name="languageCode" value="{@$languageCode}">