Skip to content

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

  1. Checks that the folder is directly under C:\Program Files (x86)\PHP or C:\Program Files\PHP.
  2. Reads C:\Windows\System32\inetsrv\config\applicationHost.config and 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
  3. Report: writes the list to the action log and stops.
  4. 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

  1. Save the script at the bottom of this page as Remove-IISPhpHandler.ps1.
  2. In the Tanium Console, go to Administration > Content > Packages.
  3. 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
  1. 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
  1. 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.

  1. Save the rollback script at the bottom of this page as Restore-IISConfigBackup.ps1.
  2. 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

  1. Deploy with Mode = Report to the endpoint. Read the action log and confirm only the expected handler and FastCGI entries are listed.
  2. Get the site owner's approval.
  3. Deploy again with Mode = Remove. Write down the backup name from the log.
  4. 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.config are not removed. The script only edits applicationHost.config. If IIS - PHP Usage still shows the old folder for a site after Remove, the mapping is in that site's own web.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
}