Zum Inhalt springen

~/CodeBlog.at

PHP: aktuelle URL auslesen – sicher, mit HTTPS & Port

·3 Min. Lesezeit

Veröffentlicht
Überarbeitet

Was ist neu: Snippet korrigiert (Port-Bug bei HTTPS, HTTP_HOST statt SERVER_NAME), Proxy- und Sicherheitshinweise sowie Framework-Varianten ergänzt.

Kurz & knapp

Die aktuelle URL setzt sich in PHP aus Protokoll, $_SERVER['HTTP_HOST'] und $_SERVER['REQUEST_URI'] zusammen. Den Host gegen eine Liste erlaubter Domains prüfen, hinter einem Proxy X-Forwarded-Proto auswerten und die URL bei der Ausgabe immer mit htmlspecialchars() maskieren. In Frameworks gibt es fertige Methoden, z. B. $request->getUri() in Symfony.

Getestet mit: PHP 8.5

Inhalt
  1. Das Snippet
  2. Was am alten Snippet falsch war
  3. Welcher $_SERVER-Wert enthält was?
  4. Hinter Proxy, Load Balancer oder Cloudflare
  5. So geht’s in Frameworks
  6. TYPO3
  7. Symfony
  8. Laravel

Ob für Canonical-Tags, Weiterleitungen nach dem Login oder Share-Buttons: Die aktuell aufgerufene URL braucht man in PHP immer wieder. PHP liefert sie nicht als fertigen Wert – sie muss aus mehreren Einträgen in $_SERVER zusammengesetzt werden. Dabei gibt es ein paar Fallstricke, an denen auch die erste Version dieses Snippets gescheitert ist.

Das Snippet

<?php
declare(strict_types=1);

/**
 * Liefert die aktuell aufgerufene URL, z. B. https://example.com/blog/?page=2
 *
 * @param string[] $allowedHosts erlaubte Hostnamen (Schutz vor Host-Header-Injection)
 * @param bool     $trustProxy   X-Forwarded-Proto auswerten (nur hinter eigenem Proxy!)
 */
function getCurrentUrl(array $allowedHosts, bool $trustProxy = false): string
{
    $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
        || (int) ($_SERVER['SERVER_PORT'] ?? 0) === 443;

    if ($trustProxy && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
        $https = strtolower(explode(',', $_SERVER['HTTP_X_FORWARDED_PROTO'])[0]) === 'https';
    }

    // HTTP_HOST enthält bei Nicht-Standard-Ports bereits den Port, z. B. "localhost:8080"
    $host = strtolower($_SERVER['HTTP_HOST'] ?? '');
    $hostname = preg_replace('/:\d+$/', '', $host);

    if (!in_array($hostname, $allowedHosts, true)) {
        $host = $allowedHosts[0];
    }

    return ($https ? 'https' : 'http') . '://' . $host . ($_SERVER['REQUEST_URI'] ?? '/');
}

Verwendung:

$url = getCurrentUrl(['example.com', 'www.example.com']);

// bei der Ausgabe in HTML immer maskieren!
echo '<link rel="canonical" href="' . htmlspecialchars($url, ENT_QUOTES) . '">';

Getestet mit PHP 8.5 – Ergebnisse für typische Server-Konstellationen:

Situation Ergebnis
HTTPS auf Port 443 https://example.com/blog/?page=2
HTTP auf Port 8080 http://example.com:8080/a
hinter Proxy mit X-Forwarded-Proto: https https://example.com/
gefälschter Host evil.test https://example.com/x (Fallback auf erlaubte Domain)
HTTPS = off http://www.example.com/

Was am alten Snippet falsch war

Die ursprüngliche Version aus 2017 setzte die URL aus SERVER_NAME und SERVER_PORT zusammen und hängte den Port an, sobald er nicht 80 war. Das führte zu drei Problemen:

  1. Port-Bug bei HTTPS: Auf Port 443 entstand https://example.com:443/… – technisch gültig, aber unschön und für Canonical-URLs ungeeignet.
  2. Falscher Host: SERVER_NAME stammt aus der Webserver-Konfiguration und ist nicht zwingend die Domain, die aufgerufen wurde – etwa bei Aliases oder www-Varianten.
  3. HTTPS = off: Laut PHP-Handbuch ist HTTPS bei verschlüsselten Aufrufen „nicht leer“. Manche Server-Setups setzen den Wert bei HTTP aber explizit auf off – ein reines isset() hält das fälschlich für HTTPS. Das neue Snippet prüft deshalb auch darauf.

Welcher $_SERVER-Wert enthält was?

Beispiel-Aufruf: https://example.com:8443/blog/artikel.php?page=2

Schlüssel Inhalt
HTTP_HOST example.com:8443 – Host inkl. Port (vom Client gesendet)
SERVER_NAME Hostname laut Webserver-Konfiguration
SERVER_PORT 8443
HTTPS nicht leer (meist on), wenn per HTTPS aufgerufen
REQUEST_URI /blog/artikel.php?page=2 – Pfad inkl. Query-String
QUERY_STRING page=2
SCRIPT_NAME /blog/artikel.php

Hinter Proxy, Load Balancer oder Cloudflare

Läuft die Seite hinter einem Reverse Proxy – Cloudflare, ein Load Balancer oder auch ein lokaler Docker-Router –, endet die verschlüsselte Verbindung oft schon dort. PHP sieht dann eine normale HTTP-Verbindung, und $_SERVER['HTTPS'] ist leer. Das ursprüngliche Protokoll übermittelt der Proxy im Header X-Forwarded-Proto, in PHP als $_SERVER['HTTP_X_FORWARDED_PROTO'].

So geht’s in Frameworks

Frameworks erledigen all das bereits – inklusive Proxy-Erkennung, sofern die vertrauenswürdigen Proxies konfiguriert sind.

TYPO3

In TYPO3 stellt der PSR-7-Request die normalisierten Server-Parameter bereit. Sie berücksichtigen auch Reverse-Proxy-Konstellationen und ersetzen das ältere GeneralUtility::getIndpEnv():

/** @var \TYPO3\CMS\Core\Http\NormalizedParams $normalizedParams */
$normalizedParams = $request->getAttribute('normalizedParams');

$url = $normalizedParams->getRequestUrl(); // z. B. https://example.com/blog/?page=2
$uri = $normalizedParams->getRequestUri(); // z. B. /blog/?page=2

Symfony

$url = $request->getUri();                        // mit Query-String
$base = $request->getSchemeAndHttpHost();          // https://example.com

Hinter einem Proxy müssen dessen IP-Adressen über trusted_proxies bzw. Request::setTrustedProxies() freigegeben sein.

Laravel

$url = url()->current();     // ohne Query-String
$full = url()->full();       // mit Query-String

// oder über das Request-Objekt
$url = $request->url();
$full = $request->fullUrl();

Häufige Fragen

Was ist der Unterschied zwischen HTTP_HOST und SERVER_NAME?

HTTP_HOST ist der Host, den der Browser im Request mitschickt – inklusive eines eventuellen Ports. SERVER_NAME kommt aus der Webserver-Konfiguration und muss nicht der aufgerufenen Domain entsprechen. Für die aktuelle URL ist HTTP_HOST richtig, sollte aber gegen erlaubte Domains geprüft werden.

Wie bekomme ich die URL ohne Query-String?

Mit strtok($_SERVER['REQUEST_URI'], '?') oder parse_url($url, PHP_URL_PATH) erhältst du nur den Pfad ohne ?….

Warum erkennt PHP hinter Cloudflare oder einem Load Balancer kein HTTPS?

Weil die Verbindung zwischen Proxy und Webserver oft unverschlüsselt ist. Das ursprüngliche Protokoll steht dann im Header X-Forwarded-Proto, den man aber nur auswerten darf, wenn der Request wirklich vom eigenen Proxy kommt.

Muss ich die aktuelle URL bei der Ausgabe maskieren?

Ja. REQUEST_URI enthält vom Besucher kontrollierte Zeichen. Bei der Ausgabe in HTML immer htmlspecialchars($url, ENT_QUOTES) verwenden, sonst droht Cross-Site-Scripting (XSS).

Quellen & weiterführende Links

  1. $_SERVER – Server- und Ausführungsumgebungsinformationen (öffnet in neuem Tab) – PHP-Handbuch
  2. htmlspecialchars (öffnet in neuem Tab) – PHP-Handbuch
  3. X-Forwarded-Proto (öffnet in neuem Tab) – MDN Web Docs
  4. Normalized parameters (normalizedParams) (öffnet in neuem Tab) – TYPO3 Dokumentation
  5. How to Configure Symfony to Work behind a Load Balancer or a Reverse Proxy (öffnet in neuem Tab) – Symfony Dokumentation
  6. Request.php (HttpFoundation) (öffnet in neuem Tab) – Symfony auf GitHub
  7. URL Generation (öffnet in neuem Tab) – Laravel Dokumentation

Geschrieben von

Christoph Schwob (öffnet in neuem Tab)

Ich bin Christoph Schwob, halbkreativer Entwickler aus Österreich und arbeite seit über 10 Jahren an und mit TYPO3. Hier sammle ich Lösungen, Code-Snippets und Notizen rund um TYPO3, macOS, Server, Homelab und alles andere.

Suche

Tippe einen Suchbegriff ein.