Remove IIS PHP Handler (Package)
One Tanium package that removes the IIS handler mappings and FastCGI applications that point to a PHP folder. Run it before Remove PHP Install, which refuses to delete a folder IIS still references.
It defaults to Report mode, which only lists what it would remove. Remove mode backs up the IIS configuration first, so the change can be rolled back with a second package on this page.
This stops PHP on the site
After Remove, any IIS site that used those entries can no longer run .php pages. Run IIS - PHP Usage first, and run Remove only with the site owner's approval.
Test status
Tested in a lab through Tanium on a server where IIS ran PHP 7.1. Report listed the right settings, Remove deleted them, and the rollback package put them back. PHP settings made for a single website (instead of the whole server) have not been tested yet. See Test results.
What the script does
- Checks that the folder is directly under
C:\Program Files (x86)\PHPorC:\Program Files\PHP. - Reads
C:\Windows\System32\inetsrv\config\applicationHost.configand finds:- every handler mapping whose script processor is in that folder, at server level and in
<location>sections for individual sites - every FastCGI application whose path is in that folder
- every handler mapping whose script processor is in that folder, at server level and in
- Report: writes the list to the action log and stops.
- Remove: runs
appcmd add backup PHP-Cleanup-<date-time>, removes the handler mappings, then the FastCGI applications, then reads the config again to confirm they are gone.
The log is written to the action log and to C:\Windows\Temp\Remove-IISPhpHandler.log.
Exit codes
| Exit code | Meaning |
|---|---|
0 |
Report completed, nothing references the folder, or removal succeeded. |
1 |
Failed: backup failed, appcmd returned an error, or entries are still there afterward. |
2 |
Blocked: the folder is outside the allowed roots. Nothing changed. |
Create the package
- Save the script at the bottom of this page as
Remove-IISPhpHandler.ps1. - In the Tanium Console, go to Administration > Content > Packages.
- Click Create Package and fill in:
| Field | Value |
|---|---|
| Package Name | <prefix> - Remove IIS PHP Handler (Windows) |
| Display Name | <prefix> - Remove IIS PHP Handler (Windows) |
| Description | Finds IIS handler mappings and FastCGI applications that point to a PHP folder. Report lists them. Remove backs up the IIS config (appcmd backup PHP-Cleanup-<date-time>) and deletes them. Exit 0 done or nothing found, 1 failed, 2 blocked. |
| Content Set | The content set your team uses for custom content |
| Command | cmd.exe /d /c powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -NoProfile -File Remove-IISPhpHandler.ps1 "$1" "$2" |
| Command Timeout | 5 minutes |
| Download Timeout | 5 minutes |
| Ignore Action Lock | Unchecked |
| Launch this package command in a process group | Checked |
| Files | Click Add File, choose Local File, upload Remove-IISPhpHandler.ps1 |
- Add the parameters:
| # | Type | Label | Values |
|---|---|---|---|
| 1 | Text Input | PHP install folder |
Default value C:\Program Files (x86)\PHP\v7.1 |
| 2 | Drop-Down List | Mode |
Report first, then Remove |
- Click Save.
Create the rollback package
The rollback uses its own small script instead of calling appcmd directly in the command line. Tanium URL-encodes the parameter, so a backup name such as PHP-Cleanup-20261105-142210 reaches the endpoint as PHP%2dCleanup%2d20261105%2d142210, and appcmd can't find it. The script decodes the name, checks that the backup exists, and lists the available backups when it doesn't.
- Save the rollback script at the bottom of this page as
Restore-IISConfigBackup.ps1. - In Administration > Content > Packages, click Create Package and fill in:
| Field | Value |
|---|---|
| Package Name | <prefix> - Restore IIS Config Backup (Windows) |
| Display Name | <prefix> - Restore IIS Config Backup (Windows) |
| Description | Restores an IIS configuration backup by name, for example one created by Remove IIS PHP Handler. Restores the whole IIS configuration as it was at backup time. Lists available backups if the name is not found. Exit 0 restored, 1 failed, 2 blocked. |
| Content Set | The content set your team uses for custom content |
| Command | cmd.exe /d /c powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -NoProfile -File Restore-IISConfigBackup.ps1 "$1" |
| Command Timeout | 5 minutes |
| Download Timeout | 5 minutes |
| Ignore Action Lock | Unchecked |
| Launch this package command in a process group | Checked |
| Files | Click Add File, choose Local File, upload Restore-IISConfigBackup.ps1 |
| Parameter 1 | Text Input, label Backup name, no default. Copy it from the cleanup action log, for example PHP-Cleanup-20261105-142210 |
Restore puts back the whole IIS configuration as of the backup. Any other IIS change made after the cleanup is rolled back too.
Failed to create process: Error code 0x00000057
If an action fails with this before the script runs, the Command field has a leading space or tab, usually from copying. Edit the package, clear the field, and type or paste the command again with nothing before cmd.exe.
Deploy
- Deploy with Mode =
Reportto the endpoint. Read the action log and confirm only the expected handler and FastCGI entries are listed. - Get the site owner's approval.
- Deploy again with Mode =
Remove. Write down the backup name from the log. - Run Remove PHP Install for the same folder.
Example output (illustrative)
2026-11-05T14:22:08 [INFO] Start. PHP folder: C:\Program Files (x86)\PHP\v7.1 | Mode: Remove
2026-11-05T14:22:08 [INFO] Handler: 'PHP_via_FastCGI' path=*.php [server level] -> C:\Program Files (x86)\PHP\v7.1\php-cgi.exe
2026-11-05T14:22:08 [INFO] FastCGI application: C:\Program Files (x86)\PHP\v7.1\php-cgi.exe arguments=''
2026-11-05T14:22:08 [INFO] Found 1 handler mapping(s) and 1 FastCGI application(s).
2026-11-05T14:22:10 [INFO] IIS config backed up as 'PHP-Cleanup-20261105-142210'. Rollback: appcmd restore backup "PHP-Cleanup-20261105-142210"
2026-11-05T14:22:11 [INFO] All IIS references to C:\Program Files (x86)\PHP\v7.1 removed. Backup: 'PHP-Cleanup-20261105-142210'. Exit 0.
Things to know
- Handlers in a site's
web.configare not removed. The script only editsapplicationHost.config. If IIS - PHP Usage still shows the old folder for a site after Remove, the mapping is in that site's ownweb.config. - Backups stay on the server under
C:\Windows\System32\inetsrv\backup\. Remove old ones by hand once you're sure you won't need them.
Test results
| Case | Expected | Result |
|---|---|---|
| Windows PowerShell 5.1 parser | No errors | Pass |
| Report, server-level handler and FastCGI app | Both listed, nothing changed, exit 0 | Pass |
| Remove, same setup | Backup created, both removed, exit 0 | Pass |
| Restore, first version (appcmd in the command line) | Handler and FastCGI app back | Fail. Command field had a leading tab (error 0x00000057), and the backup name arrived URL-encoded. Replaced with the rollback script |
| Restore with the rollback script | Name decoded, handler and FastCGI app back, exit 0 | Pass. PHP%2dCleanup%2d... decoded and restored, exit 0 |
| Restore with a name that doesn't exist | Exit 2, available backups listed | Not tested yet |
Handler in a <location> section for one site |
Listed and removed | Not tested yet |
| No IIS installed | Exit 0, nothing to do | Not tested yet |
Script
<#
.SYNOPSIS
Removes IIS handler mappings and FastCGI applications that point to a PHP install folder.
Built for a Tanium package.
.DESCRIPTION
Usage: Remove-IISPhpHandler.ps1 "<PHP folder>" "<Mode: Report|Remove>"
Default folder: C:\Program Files (x86)\PHP\v7.1
Default mode: Report
Report: lists every matching handler mapping (server level and <location> sections in
applicationHost.config) and every matching FastCGI application. Changes nothing.
Remove: backs up the IIS configuration with appcmd, removes the handler mappings first,
then the FastCGI applications, then re-reads the config to confirm they are gone.
After Remove, any IIS site that relied on these entries can no longer run PHP from this folder.
Run Remove only after the site owner has approved.
Rollback: %windir%\system32\inetsrv\appcmd.exe restore backup "<backup name from the log>"
.NOTES
Exit codes:
0 Report completed, nothing to remove, or removal succeeded
1 Failed (backup failed, appcmd error, or entries still present after removal)
2 Blocked: path failed validation
Log: C:\Windows\Temp\Remove-IISPhpHandler.log (also written to stdout)
#>
param(
[string]$PhpDir,
[string]$Mode
)
$ErrorActionPreference = 'Stop'
$DefaultDir = 'C:\Program Files (x86)\PHP\v7.1'
$LogFile = Join-Path $env:windir 'Temp\Remove-IISPhpHandler.log'
function Write-Log {
param([string]$Message, [string]$Level = 'INFO')
$line = '{0} [{1}] {2}' -f (Get-Date -Format 's'), $Level, $Message
Write-Output $line
Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue
}
function Test-UnderFolder {
param([string]$Value, [string]$Folder)
if ([string]::IsNullOrWhiteSpace($Value)) { return $false }
$expanded = [Environment]::ExpandEnvironmentVariables($Value)
return $expanded.StartsWith("$Folder\", [StringComparison]::OrdinalIgnoreCase)
}
function Get-PhpIisEntries {
param([string]$ConfigPath, [string]$Folder)
[xml]$cfg = Get-Content -LiteralPath $ConfigPath -Raw
# Handler mappings: server level plus any <location> sections in applicationHost.config
$scopes = @([pscustomobject]@{ Location = ''; Node = $cfg.configuration.'system.webServer' })
foreach ($loc in @($cfg.configuration.location)) {
if ($loc) { $scopes += [pscustomobject]@{ Location = [string]$loc.path; Node = $loc.'system.webServer' } }
}
$handlers = @()
foreach ($s in $scopes) {
if (-not $s.Node -or -not $s.Node.handlers) { continue }
foreach ($h in @($s.Node.handlers.add)) {
if (-not $h) { continue }
if (Test-UnderFolder -Value ([string]$h.scriptProcessor) -Folder $Folder) {
$handlers += [pscustomobject]@{
Location = $s.Location
Name = [string]$h.name
Path = [string]$h.path
ScriptProcessor = [string]$h.scriptProcessor
}
}
}
}
# FastCGI applications (server level only; this section cannot be set per site)
$fastCgi = @()
$fNode = $cfg.configuration.'system.webServer'.fastCgi
if ($fNode) {
foreach ($a in @($fNode.application)) {
if (-not $a) { continue }
if (Test-UnderFolder -Value ([string]$a.fullPath) -Folder $Folder) {
$fastCgi += [pscustomobject]@{
FullPath = [string]$a.fullPath
Arguments = [string]$a.arguments
}
}
}
}
[pscustomobject]@{ Handlers = $handlers; FastCgi = $fastCgi }
}
function Invoke-AppCmd {
param([string]$AppCmd, [string[]]$Arguments)
$out = & $AppCmd @Arguments 2>&1
$text = ($out | ForEach-Object { "$_" }) -join ' '
Write-Log "appcmd $($Arguments -join ' ') => exit $LASTEXITCODE | $text"
if ($LASTEXITCODE -ne 0) { throw "appcmd failed with exit code $LASTEXITCODE" }
}
try {
# Tanium passes parameters URL-encoded (a space arrives as %20), so decode them first.
$PhpDir = [System.Uri]::UnescapeDataString([string]$PhpDir)
$Mode = [System.Uri]::UnescapeDataString([string]$Mode)
if ([string]::IsNullOrWhiteSpace($PhpDir)) { $PhpDir = $DefaultDir }
$PhpDir = $PhpDir.Trim().Trim('"').TrimEnd('\')
$removeOn = ([string]$Mode).Trim() -ieq 'Remove'
Write-Log "Start. PHP folder: $PhpDir | Mode: $(if ($removeOn) { 'Remove' } else { 'Report' })"
# --- Path validation ----------------------------------------------------
$allowedRoots = @("$env:SystemDrive\Program Files (x86)\PHP", "$env:SystemDrive\Program Files\PHP")
if ($allowedRoots -notcontains (Split-Path -Path $PhpDir -Parent)) {
Write-Log "Blocked: folder must be directly under one of: $($allowedRoots -join ', ')" 'ERROR'
exit 2
}
# Use Sysnative when running as a 32-bit process so System32 is not redirected to SysWOW64.
$sysDir = if (Test-Path "$env:windir\Sysnative") { "$env:windir\Sysnative" } else { "$env:windir\System32" }
$appCmd = Join-Path $sysDir 'inetsrv\appcmd.exe'
$appHost = Join-Path $sysDir 'inetsrv\config\applicationHost.config'
if (-not (Test-Path -LiteralPath $appHost) -or -not (Test-Path -LiteralPath $appCmd)) {
Write-Log 'IIS not installed. Nothing to do.'
exit 0
}
# --- Find entries -------------------------------------------------------
$found = Get-PhpIisEntries -ConfigPath $appHost -Folder $PhpDir
foreach ($h in $found.Handlers) {
$scope = if ($h.Location) { "location '$($h.Location)'" } else { 'server level' }
Write-Log "Handler: '$($h.Name)' path=$($h.Path) [$scope] -> $($h.ScriptProcessor)"
}
foreach ($f in $found.FastCgi) {
Write-Log "FastCGI application: $($f.FullPath) arguments='$($f.Arguments)'"
}
Write-Log "Found $($found.Handlers.Count) handler mapping(s) and $($found.FastCgi.Count) FastCGI application(s)."
if ($found.Handlers.Count -eq 0 -and $found.FastCgi.Count -eq 0) {
Write-Log 'Nothing references this folder. Done. Exit 0.'
exit 0
}
if (-not $removeOn) {
Write-Log 'Report mode. No changes made. Rerun with Mode = Remove to delete these entries. Exit 0.'
exit 0
}
# --- Backup IIS config --------------------------------------------------
$backupName = 'PHP-Cleanup-{0}' -f (Get-Date -Format 'yyyyMMdd-HHmmss')
Invoke-AppCmd -AppCmd $appCmd -Arguments @('add', 'backup', $backupName)
Write-Log "IIS config backed up as '$backupName'. Rollback: appcmd restore backup `"$backupName`""
# --- Remove handler mappings first, then FastCGI applications -----------
foreach ($h in $found.Handlers) {
$a = @('set', 'config')
if ($h.Location) { $a += $h.Location }
$a += @('-section:system.webServer/handlers', "/-[name='$($h.Name)']", '/commit:apphost')
Invoke-AppCmd -AppCmd $appCmd -Arguments $a
}
foreach ($f in $found.FastCgi) {
$key = if ($f.Arguments) { "[fullPath='$($f.FullPath)',arguments='$($f.Arguments)']" } else { "[fullPath='$($f.FullPath)']" }
Invoke-AppCmd -AppCmd $appCmd -Arguments @('set', 'config', '-section:system.webServer/fastCgi', "/-$key", '/commit:apphost')
}
# --- Verify -------------------------------------------------------------
$after = Get-PhpIisEntries -ConfigPath $appHost -Folder $PhpDir
if ($after.Handlers.Count -gt 0 -or $after.FastCgi.Count -gt 0) {
Write-Log "Still present after removal: $($after.Handlers.Count) handler(s), $($after.FastCgi.Count) FastCGI app(s)." 'ERROR'
exit 1
}
Write-Log "All IIS references to $PhpDir removed. Backup: '$backupName'. Exit 0."
exit 0
}
catch {
Write-Log "Failed: $($_.Exception.Message)" 'ERROR'
exit 1
}
Rollback script
<#
.SYNOPSIS
Restores an IIS configuration backup by name. Built for a Tanium package.
.DESCRIPTION
Usage: Restore-IISConfigBackup.ps1 "<backup name>"
Example: Restore-IISConfigBackup.ps1 "PHP-Cleanup-20261105-094747"
Restores the whole IIS configuration as it was when the backup was taken,
using appcmd restore backup. Lists the available backups when the name is not found.
.NOTES
Exit codes:
0 Restored
1 Failed (appcmd returned an error)
2 Blocked: name missing or invalid, backup not found, or IIS not installed
Log: C:\Windows\Temp\Restore-IISConfigBackup.log (also written to stdout)
#>
param([string]$BackupName)
$ErrorActionPreference = 'Stop'
$LogFile = Join-Path $env:windir 'Temp\Restore-IISConfigBackup.log'
function Write-Log {
param([string]$Message, [string]$Level = 'INFO')
$line = '{0} [{1}] {2}' -f (Get-Date -Format 's'), $Level, $Message
Write-Output $line
Add-Content -Path $LogFile -Value $line -ErrorAction SilentlyContinue
}
try {
# Tanium passes parameters URL-encoded (a hyphen arrives as %2d), so decode it first.
$BackupName = [System.Uri]::UnescapeDataString([string]$BackupName).Trim().Trim('"')
Write-Log "Start. Backup name: $BackupName"
if ($BackupName -notmatch '^[A-Za-z0-9._-]+$') {
Write-Log 'Blocked: backup name is empty or has characters other than letters, numbers, dot, underscore, and hyphen.' 'ERROR'
exit 2
}
# Use Sysnative when running as a 32-bit process so System32 is not redirected to SysWOW64.
$sysDir = if (Test-Path "$env:windir\Sysnative") { "$env:windir\Sysnative" } else { "$env:windir\System32" }
$appCmd = Join-Path $sysDir 'inetsrv\appcmd.exe'
$backupDir = Join-Path $sysDir 'inetsrv\backup'
if (-not (Test-Path -LiteralPath $appCmd)) {
Write-Log 'Blocked: IIS is not installed.' 'ERROR'
exit 2
}
if (-not (Test-Path -LiteralPath (Join-Path $backupDir $BackupName))) {
Write-Log "Blocked: backup '$BackupName' not found in $backupDir." 'ERROR'
$available = @(Get-ChildItem -LiteralPath $backupDir -Directory -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -ExpandProperty Name)
if ($available.Count -gt 0) {
Write-Log "Available backups (newest first): $($available -join ', ')"
} else {
Write-Log 'No IIS backups exist on this endpoint.'
}
exit 2
}
$out = & $appCmd restore backup $BackupName 2>&1
$text = ($out | ForEach-Object { "$_" }) -join ' '
Write-Log "appcmd restore backup $BackupName => exit $LASTEXITCODE | $text"
if ($LASTEXITCODE -ne 0) {
Write-Log 'Restore failed.' 'ERROR'
exit 1
}
Write-Log 'Restored. Exit 0.'
exit 0
}
catch {
Write-Log "Failed: $($_.Exception.Message)" 'ERROR'
exit 1
}