ISW-Tools
Jörn Walter Veröffentlicht 14 Min. Lesezeit Security
Der Windows Papst · Webserver & PHP · 26.10.2026

PHP unter Windows Server und IIS sicher betreiben: php.ini härten Schritt für Schritt

PHP läuft auf Windows Server zuverlässig – vorausgesetzt, es ist richtig eingebunden und abgesichert. In der Praxis finden sich aber immer wieder XAMPP-Installationen auf Produktivservern, Fehlermeldungen mit vollständigen Pfaden im Browser, beschreibbare Webverzeichnisse und eine php.ini, die seit Jahren niemand angesehen hat. Dieser Beitrag zeigt Schritt für Schritt, wie Sie PHP unter IIS sicher betreiben: von der richtigen Architektur über NTFS-Rechte und eine gehärtete php.ini bis zu den IIS-Einstellungen und einem Prüfskript für den Soll-Ist-Vergleich.

Hintergrund: Was CVE-2024-4577 gelehrt hat

Im Juni 2024 wurde mit CVE-2024-4577 eine kritische Lücke in PHP unter Windows bekannt. Über die Windows-Zeichenkonvertierung „Best-Fit“ konnten Angreifer ohne Anmeldung Befehlszeilenargumente an php-cgi.exe einschleusen und so beliebigen Code ausführen – eine Umgehung des Schutzes gegen die alte Lücke CVE-2012-1823. Betroffen war vor allem PHP im klassischen CGI-Modus, wie er etwa bei XAMPP unter Windows mit Apache verwendet wird. Angriffsversuche wurden innerhalb von 24 Stunden beobachtet, und die Ransomware-Gruppe TellYouThePass nutzte die Lücke kurz darauf aktiv aus. Behoben wurde sie in PHP 8.3.8, 8.2.20 und 8.1.29.

Die Lehren daraus sind die Grundlage dieses Beitrags:

  • PHP nie im CGI-Modus betreiben, sondern unter IIS über FastCGI.
  • XAMPP und vergleichbare Entwicklungspakete gehören nicht auf Server.
  • Nur unterstützte PHP-Versionen einsetzen und Updates zeitnah einspielen.
  • Die Folgen einer Lücke durch minimale Rechte begrenzen.

Die Zielarchitektur

Komponente Empfehlung Begründung
Webserver IIS mit Rollendienst „CGI“ (enthält das FastCGI-Modul) Von Microsoft gepflegt, sauber in Windows-Updates und GPO-Verwaltung eingebunden
PHP-Build Offizieller x64 Non Thread Safe (NTS)-Build von windows.php.net Laut PHP-Handbuch die richtige Variante für IIS mit FastCGI
PHP-Version Unterstützter Zweig, bevorzugt 8.4 oder 8.5 Sicherheitsupdates bis Ende 2028 bzw. 2029
Laufzeit Aktuelle Visual C++ Redistributable 2015–2022 (x64) Ohne passende Laufzeit startet PHP nicht
Anwendungspool Eigener Pool je Anwendung, Identität ApplicationPoolIdentity Trennung der Anwendungen, eigene Rechte pro Site
Ablage PHP unter C:\PHP\<Version>, Webinhalte auf eigenem Datenlaufwerk Klare Rechte, parallele Versionen, einfache Updates
Wichtig: Prüfen Sie nach dem Download die SHA256-Prüfsumme mit der Angabe auf windows.php.net. Laden Sie PHP-Builds niemals von Drittseiten oder aus Foren.

Schritt 1: IIS und PHP installieren

# IIS mit CGI/FastCGI, Request Filtering und Management-Tools
Install-WindowsFeature Web-Server, Web-CGI, Web-Filtering, Web-Http-Logging, `
    Web-Http-Redirect, Web-Mgmt-Console, Web-Scripting-Tools

# PHP-Verzeichnisse anlegen (ZIP vorher prüfen und nach C:\PHP\8.4 entpacken)
New-Item -ItemType Directory -Path C:\PHP\8.4, C:\PHP\temp, C:\PHP\logs, C:\PHP\sessions -Force | Out-Null
Copy-Item C:\PHP\8.4\php.ini-production C:\PHP\8.4\php.ini

# FastCGI-Anwendung registrieren
Import-Module WebAdministration
$php = 'C:\PHP\8.4\php-cgi.exe'
Add-WebConfiguration -Filter 'system.webServer/fastCgi' -Value @{
    fullPath = $php; instanceMaxRequests = 10000; activityTimeout = 600; requestTimeout = 120
}
Add-WebConfiguration -Filter "system.webServer/fastCgi/application[@fullPath='$php']/environmentVariables" -Value @(
    @{ name = 'PHP_FCGI_MAX_REQUESTS'; value = '10000' },
    @{ name = 'PHPRC'; value = 'C:\PHP\8.4' }
)

# Handler nur für die Site, die PHP braucht – nicht global auf Serverebene
New-WebHandler -PSPath 'IIS:\Sites\Intranet' -Name 'PHP_via_FastCGI' -Path '*.php' -Verb 'GET,HEAD,POST' `
    -Modules FastCgiModule -ScriptProcessor $php -ResourceType File
Praxistipp: -ResourceType File sorgt dafür, dass IIS PHP nur aufruft, wenn die angeforderte Datei tatsächlich existiert. Das verhindert eine ganze Klasse von Pfadtricks. Die Verben sind auf GET, HEAD und POST beschränkt – PUT oder DELETE braucht eine klassische PHP-Anwendung nicht. Für REST-Anwendungen, die weitere Verben brauchen, passen Sie die Liste gezielt an.

Schritt 2: NTFS-Rechte nach dem Minimalprinzip

Der wichtigste Schutz gegen die Folgen einer Lücke ist, dass PHP fast nichts schreiben darf. Die folgenden Befehle verwenden SIDs statt Gruppennamen und funktionieren deshalb auf deutschen und englischen Servern gleichermaßen (S-1-5-32-544 = Administratoren, S-1-5-18 = SYSTEM, S-1-5-32-568 = IIS_IUSRS).

# PHP-Programmverzeichnis: nur lesen und ausführen
icacls C:\PHP\8.4 /inheritance:r /grant:r "*S-1-5-32-544:(OI)(CI)F" "*S-1-5-18:(OI)(CI)F" "*S-1-5-32-568:(OI)(CI)RX"

# Temp-, Sitzungs- und Logverzeichnisse: nur der jeweilige Anwendungspool darf schreiben
foreach ($d in 'C:\PHP\temp','C:\PHP\sessions','C:\PHP\logs') {
    icacls $d /inheritance:r /grant:r "*S-1-5-32-544:(OI)(CI)F" "*S-1-5-18:(OI)(CI)F" "IIS AppPool\Intranet:(OI)(CI)M"
}

# Webinhalt: Anwendungspool nur lesen
icacls D:\Web\Intranet /inheritance:r /grant:r "*S-1-5-32-544:(OI)(CI)F" "*S-1-5-18:(OI)(CI)F" "IIS AppPool\Intranet:(OI)(CI)RX"

# Ausnahme: Upload-Verzeichnis beschreibbar – dort aber KEINE Skriptausführung (siehe Schritt 4)
icacls D:\Web\Intranet\uploads /grant "IIS AppPool\Intranet:(OI)(CI)M"
Achtung: Verwenden Sie für Temp und Sitzungen nicht das allgemeine C:\Windows\Temp. Sitzungsdateien anderer Anwendungen wären sonst für jeden Anwendungspool lesbar. Bei mehreren PHP-Anwendungen auf einem Server erhält jede Anwendung eigene Temp- und Sitzungsverzeichnisse.

Schritt 3: Die php.ini härten

Ausgangspunkt ist immer php.ini-production, die bereits sichere Grundwerte enthält. Die folgende Tabelle zeigt die sicherheitsrelevanten Direktiven, ihren Zielwert und den Grund.

Informationspreisgabe und Fehlerbehandlung

Direktive Zielwert Warum
expose_php Off Unterdrückt den Header X-Powered-By: PHP/8.x.y – Angreifer erfahren die genaue Version nicht
display_errors Off Keine Fehlermeldungen mit Pfaden, SQL-Befehlen oder Variablen im Browser
display_startup_errors Off Gleiches für Fehler beim Start
log_errors On Fehler landen im Protokoll statt beim Besucher
error_log C:\PHP\logs\php_errors.log Zentrales, nur für Administratoren lesbares Protokoll
error_reporting E_ALL Alle Meldungen protokollieren, auch Deprecations für das nächste Update
zend.exception_ignore_args On Keine Funktionsargumente (z. B. Kennwörter) in Stacktraces
mail.add_x_header Off Kein Skriptpfad im Header versendeter E-Mails

Entfernte Inhalte und Dateisystem

Direktive Zielwert Warum
allow_url_include Off Verhindert das Einbinden von Code über URLs (Remote File Inclusion). Seit PHP 7.4 veraltet und muss aus bleiben
allow_url_fopen Off, wenn möglich Verhindert, dass Dateifunktionen entfernte URLs öffnen. Vorher testen: Manche Anwendungen laden darüber Updates; WordPress nutzt in der Regel cURL
open_basedir D:\Web\Intranet;C:\PHP\temp;C:\PHP\sessions PHP darf nur innerhalb dieser Pfade lesen und schreiben (unter Windows mit Semikolon getrennt)
upload_tmp_dir C:\PHP\temp Eigenes Upload-Verzeichnis innerhalb von open_basedir
sys_temp_dir C:\PHP\temp Eigenes Temp-Verzeichnis statt Systemtemp
disable_functions exec,passthru,shell_exec,system,proc_open,popen,show_source Kein Ausführen von Betriebssystembefehlen aus PHP heraus

Ressourcen und Uploads

Direktive Richtwert Warum
file_uploads On nur bei Bedarf Anwendungen ohne Upload-Funktion brauchen es nicht
upload_max_filesize / post_max_size so klein wie möglich, z. B. 16M / 20M Begrenzt Speicher- und Plattenmissbrauch
max_file_uploads z. B. 10 Begrenzung der Dateien pro Anfrage
max_execution_time 30 Bricht hängende oder missbrauchte Skripte ab
max_input_time 60 Begrenzt die Verarbeitungszeit für Eingabedaten
max_input_vars 1000 (Standard) Schutz vor sehr großen Formular- und Hash-Kollisionsangriffen
memory_limit z. B. 256M Kein unbegrenzter Speicherverbrauch einzelner Skripte

Sitzungen (Sessions)

Direktive Zielwert Warum
session.save_path C:\PHP\sessions Eigenes, geschütztes Verzeichnis je Anwendung
session.use_strict_mode 1 Nicht vom Server erzeugte Sitzungs-IDs werden abgelehnt (Schutz vor Session Fixation)
session.use_only_cookies 1 Sitzungs-ID nie in der URL
session.use_trans_sid 0 Keine automatische Weitergabe der Sitzungs-ID in Links
session.cookie_httponly 1 Cookie für JavaScript unsichtbar – erschwert Cookie-Diebstahl über XSS
session.cookie_secure 1 Cookie nur über HTTPS (setzt HTTPS voraus)
session.cookie_samesite Lax oder Strict Schutz vor Cross-Site Request Forgery

IIS- und FastCGI-spezifische Werte

Direktive Wert unter IIS Erklärung
cgi.force_redirect 0 Der Kommentar in der mitgelieferten php.ini stellt klar: Unter IIS kann und muss dieser Schutz abgeschaltet werden, weil IIS die Anfrage selbst an PHP übergibt
cgi.fix_pathinfo 1 Für die korrekte Auswertung von PATH_INFO unter IIS üblich. Die Absicherung übernimmt der Handler mit ResourceType File
fastcgi.impersonate 1 PHP arbeitet mit der Identität der Anfrage; bei anonymem Zugriff mit dem konfigurierten anonymen Benutzer bzw. der Pool-Identität
fastcgi.logging 0 Verhindert, dass PHP-Meldungen als FastCGI-Fehler an IIS gemeldet und als Fehler 500 ausgegeben werden

Leistung und Zeitzone

; --- Auszug php.ini (Produktivsystem, IIS) ---
expose_php = Off
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = "C:\PHP\logs\php_errors.log"
error_reporting = E_ALL
zend.exception_ignore_args = On

allow_url_include = Off
allow_url_fopen = Off
open_basedir = "D:\Web\Intranet;C:\PHP\temp;C:\PHP\sessions"
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,show_source
upload_tmp_dir = "C:\PHP\temp"
sys_temp_dir = "C:\PHP\temp"

file_uploads = On
upload_max_filesize = 16M
post_max_size = 20M
max_file_uploads = 10
max_execution_time = 30
max_input_time = 60
memory_limit = 256M

session.save_path = "C:\PHP\sessions"
session.use_strict_mode = 1
session.use_only_cookies = 1
session.use_trans_sid = 0
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Lax"

cgi.force_redirect = 0
cgi.fix_pathinfo = 1
fastcgi.impersonate = 1
fastcgi.logging = 0

date.timezone = "Europe/Berlin"
mail.add_x_header = Off

extension_dir = "C:\PHP\8.4\ext"
; nur benötigte Erweiterungen aktivieren, z. B.:
extension=curl
extension=mbstring
extension=openssl
extension=pdo_mysql

zend_extension=opcache
opcache.enable = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 10000
opcache.validate_timestamps = 1
opcache.revalidate_freq = 60
Hinweis: Aktivieren Sie nur die Erweiterungen, die die Anwendung tatsächlich benötigt. Jede zusätzliche Erweiterung ist zusätzlicher Code mit eigenem Fehler- und Lückenpotenzial.

Schritt 4: IIS absichern

Keine Skriptausführung im Upload-Verzeichnis

Das häufigste Muster bei kompromittierten Webanwendungen: Ein Angreifer lädt über ein Upload-Formular eine Datei bild.php hoch und ruft sie anschließend auf. Verhindern Sie das mit einer eigenen web.config im Upload-Verzeichnis:

<?xml version="1.0" encoding="UTF-8"?>
<!-- D:\Web\Intranet\uploads\web.config -->
<configuration>
  <system.webServer>
    <handlers accessPolicy="Read">
      <remove name="PHP_via_FastCGI" />
    </handlers>
    <security>
      <requestFiltering>
        <fileExtensions allowUnlisted="true">
          <add fileExtension=".php" allowed="false" />
          <add fileExtension=".phtml" allowed="false" />
          <add fileExtension=".phar" allowed="false" />
        </fileExtensions>
      </requestFiltering>
    </security>
  </system.webServer>
</configuration>

Request Filtering und Header für die Site

<?xml version="1.0" encoding="UTF-8"?>
<!-- D:\Web\Intranet\web.config (Auszug) -->
<configuration>
  <system.webServer>
    <security>
      <requestFiltering removeServerHeader="true" allowDoubleEscaping="false">
        <requestLimits maxAllowedContentLength="20971520" />
        <fileExtensions allowUnlisted="true">
          <add fileExtension=".ini" allowed="false" />
          <add fileExtension=".inc" allowed="false" />
          <add fileExtension=".bak" allowed="false" />
          <add fileExtension=".sql" allowed="false" />
          <add fileExtension=".log" allowed="false" />
          <add fileExtension=".env" allowed="false" />
        </fileExtensions>
        <hiddenSegments>
          <add segment=".git" />
          <add segment="vendor" />
          <add segment="config" />
        </hiddenSegments>
        <verbs allowUnlisted="false">
          <add verb="GET" allowed="true" />
          <add verb="HEAD" allowed="true" />
          <add verb="POST" allowed="true" />
        </verbs>
      </requestFiltering>
    </security>
    <httpProtocol>
      <customHeaders>
        <remove name="X-Powered-By" />
        <add name="X-Content-Type-Options" value="nosniff" />
        <add name="X-Frame-Options" value="SAMEORIGIN" />
        <add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
        <add name="Strict-Transport-Security" value="max-age=31536000" />
      </customHeaders>
    </httpProtocol>
    <directoryBrowse enabled="false" />
    <httpErrors errorMode="DetailedLocalOnly" />
  </system.webServer>
</configuration>
Achtung: maxAllowedContentLength (Bytes) muss zu post_max_size in der php.ini passen, sonst lehnt IIS größere Uploads bereits vor PHP mit Fehler 404.13 ab. Die Segmente unter hiddenSegments sind Beispiele – prüfen Sie, welche Verzeichnisse Ihre Anwendung tatsächlich über das Web ausliefern muss. removeServerHeader setzt IIS 10 ab Windows Server 2016 Version 1709 voraus.

HTTPS und TLS

Ohne HTTPS sind session.cookie_secure und HSTS wirkungslos. Binden Sie ein gültiges Zertifikat an die Site, leiten Sie HTTP auf HTTPS um und deaktivieren Sie auf Serverebene veraltete Protokolle wie TLS 1.0/1.1 und schwache Cipher Suites über die Schannel-Konfiguration.

Schritt 5: Konfiguration prüfen

Ein Soll-Ist-Vergleich gehört nach jeder Änderung und jedem Update dazu. Das folgende Skript liest die wirksamen Werte mit derselben php.ini aus und meldet Abweichungen:

$phpDir = 'C:\PHP\8.4'
$soll = [ordered]@{
    'expose_php'='';            'display_errors'='';        'log_errors'='1'
    'allow_url_include'='';     'allow_url_fopen'=''
    'session.use_strict_mode'='1'; 'session.cookie_httponly'='1'; 'session.cookie_secure'='1'
    'session.use_only_cookies'='1'; 'cgi.force_redirect'='0';     'zend.exception_ignore_args'='1'
}
# PHP-Code ohne doppelte Anführungszeichen, damit Windows PowerShell 5.1 nichts verschluckt
$code = 'foreach ($argv as $i => $k) { if ($i) echo $k, ''='', ini_get($k), PHP_EOL; }'
$ist = & "$phpDir\php.exe" -c "$phpDir\php.ini" -r $code -- @($soll.Keys) |
       Out-String | ConvertFrom-StringData

foreach ($k in $soll.Keys) {
    $ok = ($ist[$k] -eq $soll[$k]) -or ($soll[$k] -eq '' -and $ist[$k] -in '', '0', 'Off')
    '{0,-28} Soll: {1,-4} Ist: {2,-4} {3}' -f $k, $soll[$k], $ist[$k], ($(if ($ok) {'OK'} else {'ABWEICHUNG'}))
}

# disable_functions und open_basedir zusätzlich anzeigen
& "$phpDir\php.exe" -c "$phpDir\php.ini" -r 'echo ini_get(''disable_functions''), PHP_EOL, ini_get(''open_basedir'');'

# Header von außen prüfen: X-Powered-By und Server dürfen nicht erscheinen
(Invoke-WebRequest -Uri 'https://intranet.firma.local/' -Method Head -UseBasicParsing).Headers
Hinweis: Die CLI liest mit -c dieselbe php.ini wie php-cgi.exe, zeigt aber für einige Werte (z. B. max_execution_time, das in der CLI immer 0 ist) abweichende Ergebnisse. Für eine abschließende Kontrolle der Webumgebung rufen Sie kurz eine Seite mit phpinfo() von intern auf und löschen sie sofort danach wieder.

Schritt 6: Betrieb, Protokolle und Updates

  • Protokolle auswerten: PHP-Fehlerprotokoll und IIS-Protokolle regelmäßig sichten, auf Spitzen bei Fehler 404/500 und auf Aufrufe nicht existierender .php-Dateien achten – ein typisches Zeichen für Scans.
  • Log-Rotation: php_errors.log wächst unbegrenzt. Per geplanter Aufgabe archivieren oder kürzen.
  • Updates: Sicherheitsankündigungen auf php.net verfolgen. PHP-Updates unter Windows erfolgen nicht über Windows Update, sondern manuell oder per Softwareverteilung.
  • Update-Verfahren: Neue Patchversion parallel entpacken, php.ini übernehmen, FastCGI-Pfad umstellen, testen, alte Version entfernen.
  • Webanwendungen aktuell halten: Die beste php.ini schützt nicht vor einer Lücke im CMS oder Plugin.

Fallstricke aus der Praxis

Fallstrick Symptom Lösung
disable_functions zu streng Backup-, Bildbearbeitungs- oder Update-Funktionen versagen Funktionen gezielt nur für die betroffene Anwendung freigeben, nie pauschal
open_basedir ohne Temp-Verzeichnisse Uploads oder Sitzungen funktionieren nicht Temp-, Sitzungs- und Upload-Pfade in open_basedir aufnehmen
session.cookie_secure ohne HTTPS Anmeldung schlägt fehl, Sitzung geht verloren Erst HTTPS einrichten, dann aktivieren
allow_url_fopen = Off Feeds, Lizenzprüfungen oder Updates schlagen fehl Prüfen, ob die Anwendung cURL nutzen kann; sonst bewusst aktiv lassen und dokumentieren
Handler auf Serverebene PHP läuft auch in Sites, die es nicht brauchen Handler nur in der Site-Konfiguration registrieren
phpinfo-Testdatei vergessen Vollständige Konfiguration öffentlich einsehbar Testdatei sofort löschen, regelmäßig nach phpinfo suchen
Visual C++ Laufzeit fehlt Fehler 500 ohne PHP-Protokoll Aktuelle x64-Redistributable installieren

Compliance-Bezug

  • BSI IT-Grundschutz: APP.3.2 Webserver (sichere Konfiguration, Schutz vor unerlaubtem Zugriff, keine Informationspreisgabe), APP.3.1 Webanwendungen und Webservices, SYS.1.2.3 Windows Server sowie OPS.1.1.3 Patch- und Änderungsmanagement.
  • NIS2: Art. 21 Abs. 2 der NIS2-Richtlinie verlangt angemessene technische Maßnahmen, u. a. zur Sicherheit bei Erwerb, Entwicklung und Wartung von Systemen sowie zum Umgang mit Schwachstellen.
  • CIS Controls v8: Control 4 (Secure Configuration of Enterprise Assets and Software) und Control 7 (Continuous Vulnerability Management).
  • OWASP: Die Härtung adressiert direkt Kategorien der OWASP Top 10 wie „Security Misconfiguration“.

Fazit

PHP auf Windows Server ist kein Sicherheitsrisiko an sich – riskant sind CGI-Betrieb, Entwicklungspakete auf Produktivsystemen, veraltete Versionen und zu großzügige Rechte. Mit FastCGI und NTS-Build, eigenen Anwendungspools, restriktiven NTFS-Rechten, einer gehärteten php.ini, Request Filtering und einem festen Update-Verfahren erreichen Sie ein Niveau, das auch einer Prüfung nach IT-Grundschutz standhält. Planen Sie für die Umsetzung pro Anwendung einen halben Tag inklusive Test ein – und prüfen Sie die Werte nach jedem Update erneut mit dem Skript.

Passende ISW-Tools

Quellen: PHP-Handbuch – Installation unter IIS · PHP-Handbuch – php.ini-Direktiven · php.net – Supported Versions · The Hacker News – CVE-2024-4577 · Microsoft Learn – IIS Request Filtering

Porträt Jörn Walter

Jörn Walter

Inhaber von IT-Service Walter in Euskirchen, betreut Windows-Umgebungen von Unternehmen und entwickelt die ISW-ADTools.

Alle Beiträge von Jörn Walter