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 |
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
-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"
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
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>
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
-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.logwä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
- TLS/Schannel Configurator – härtet TLS-Protokolle und Cipher Suites auf dem IIS-Server nach BSI-, NIST- oder Mozilla-Vorgaben.
- Windows Firewall Manager Pro – beschränkt den Zugriff auf Webserver und Verwaltungsports zentral und nachvollziehbar.
- CVE Scanner – NIST Database – erkennt veraltete und verwundbare Softwarestände auf Ihren Servern.
- Windows Live-Ereignismonitor – überwacht die Ereignisprotokolle Ihrer Webserver in Echtzeit.
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

