Windows Security Advanced Tab showing file permissions for WordPress on IIS

Fixing WordPress File Permissions Issues for Windows IIS

Solving WordPress file permissions for Windows IIS: A Two-Layer Solution

WordPress file permissions for Windows IIS can be tricky. If you’ve tried running WordPress on Windows with IIS (Internet Information Services), you may have encountered frustrating permission issues, particularly when trying to update WordPress or install plugins. The key challenge lies in finding the right balance between giving WordPress the permissions it needs to update and maintaining the security of your site. In this article, we’ll walk through a solution to address these issues, explaining the steps taken and why they’re necessary.

The Problem: WordPress file permissions Windows IIS

WordPress file permissions Windows IIS

On a Windows server, WordPress needs certain file permissions to install updates, as WordPress frequently needs to write to multiple files and directories during an update. However, allowing IUSR (the built-in IIS anonymous user account) to have Write permissions on all files in the WordPress installation creates a significant security hole. This is especially concerning because thousands of files may need to be updated at once, and granting broad write permissions to IUSR leaves those files vulnerable to exploitation by malicious actors.

After some trial and error, we found that granting IUSR Write permissions to all files was the only way to satisfy WordPress’s Site Health requirements and allow updates to complete successfully. But this approach was not ideal from a security standpoint. So, we needed to add additional layers of security to reduce the risk.

A Two-Layer Solution

To address this, we implemented a two-layer security solution that balances functionality with security:

Layer 1: The IIS Configuration Layer

The first layer involves configuring IIS to block potentially dangerous HTTP requests during the period when IUSR permissions are temporarily granted. This provides an additional layer of defense by restricting access to sensitive PHP files and HTTP methods that could be exploited by attackers.

What We’re Blocking and Why:
  1. Blocking wp-includes/*.php:
    • The wp-includes directory contains critical internal WordPress files that should never be directly accessed via HTTP. Exposing these files can lead to remote code execution vulnerabilities.
    • We block all .php files in this directory from external HTTP requests.
  2. Blocking Root PHP Files Except for Key Exceptions:
    • Certain WordPress PHP files in the root directory (such as index.php, wp-login.php, and wp-cron.php) are necessary for the site’s operation. However, exposing other PHP files in the root directory increases the attack surface.
    • We block all root .php files except for the following:
      • index.php
      • wp-login.php
      • wp-cron.php
      • wp-signup.php, wp-activate.php
      • Site health and admin AJAX files: wp-admin/site-health.php, wp-admin/admin-ajax.php
  3. Blocking Dangerous HTTP Methods:
    • HTTP methods such as PUT, DELETE, and PROPFIND can be exploited if exposed to the public web. These methods allow attackers to upload files, delete files, or probe for sensitive information.
    • We block these methods to prevent unauthorized actions.
The web.config File:

The following configuration blocks the aforementioned HTTP requests:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<security>
<requestFiltering>
<verbs allowUnlisted="false">
<add verb="GET" allowed="true" />
<add verb="POST" allowed="true" />
<add verb="HEAD" allowed="true" />
<add verb="PUT" allowed="false" />
<add verb="DELETE" allowed="false" />
<add verb="PROPFIND" allowed="false" />
</verbs>
</requestFiltering>
</security>
<rewrite>
<rules>
<rule name="Block wp-includes PHP" stopProcessing="true">
<match url="^wp-includes/.*\.php$" />
<action type="AbortRequest" />
</rule>
<rule name="Allow wp-admin PHP" stopProcessing="true">
<match url="^wp-admin/.*\.php$" />
<action type="None" />
</rule>
<rule name="Block root PHP except allowed" stopProcessing="true">
<match url="^.*\.php$" />
<conditions logicalGrouping="MatchAll">
<!-- Allow specific root PHP files -->
<add input="{REQUEST_URI}" pattern="^/(index\.php|wp-login\.php|wp-cron\.php|wp-signup\.php|wp-activate\.php)($|\?.*)" negate="true" />
<!-- Exclude wp-includes (handled separately) -->
<add input="{REQUEST_URI}" pattern="^/wp-includes/" negate="true" />
</conditions>
<action type="AbortRequest" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>

Layer 2: PowerShell Scripts for Unlocking and Locking Permissions

The second layer involves the use of two PowerShell scripts to temporarily grant the necessary permissions for WordPress updates and then revert them once the update is complete. This approach ensures that IUSR has write access only when absolutely necessary, reducing the exposure time for potential security risks.

UnlockWPSite.ps1:

This script temporarily grants Modify+Write permissions to IUSR on critical WordPress directories (e.g., wp-content\uploads, wp-content\plugins, wp-content\themes, wp-content\cache) to enable WordPress updates.

powershellCopy# Script to grant Modify+Write permissions for IUSR to necessary directories
param (
    [Parameter(Mandatory = $true)]
    [string]$SiteName,
    [switch]$DryRun
)

# Granting function for write access
function Grant-WriteAccess($target) {
    if ($DryRun) {
        Write-Host "[DryRun] Would grant Modify+Write to IUSR on $target"
    } else {
        Write-Host "Granting Modify+Write to IUSR on $target"
        icacls $target /grant:r "IUSR:(M,W)" /t | Out-Null
    }
}

# Define site and subdirectories
$basePath = "C:\inetpub\wwwroot"
$sitePath = Join-Path $basePath $SiteName
$subDirs = @("wp-content\uploads", "wp-content\plugins", "wp-content\themes", "wp-content\cache")

# Grant permissions to necessary directories
foreach ($sub in $subDirs) {
    $fullPath = Join-Path $sitePath $sub
    if (Test-Path $fullPath -PathType Container) {
        Grant-WriteAccess -target $fullPath
    } elseif ($DryRun) {
        Write-Host "[DryRun] Directory missing: $fullPath"
    }
}

Write-Host "✅ Permissions update complete."
LockWPSite.ps1:

After updates are completed, this script removes IUSR permissions from the root site folder and ensures write access is only granted to necessary directories.

powershellCopy# Script to remove Modify+Write permissions for IUSR after updates
param (
    [Parameter(Mandatory = $true)]
    [string]$SiteName,
    [switch]$DryRun
)

# Remove function for IUSR
function Remove-IUSR($target) {
    if ($DryRun) {
        Write-Host "[DryRun] Would remove IUSR from $target"
    } else {
        Write-Host "Removing IUSR from $target"
        icacls $target /remove IUSR /t | Out-Null
    }
}

# Define site and writable subdirectories
$basePath = "C:\inetpub\wwwroot"
$sitePath = Join-Path $basePath $SiteName
$writableSubDirs = @("wp-content\uploads", "wp-content\plugins", "wp-content\themes", "wp-content\cache")

# Remove IUSR from the root and lock writable subdirectories
Remove-IUSR -target $sitePath
foreach ($sub in $writableSubDirs) {
    $fullPath = Join-Path $sitePath $sub
    if (Test-Path $fullPath -PathType Container) {
        Grant-WriteAccess -target $fullPath
    }
}

Write-Host "✅ Lock complete."

Final Thoughts

This two-layer solution strikes a balance between the need to update WordPress and the desire to maintain a secure environment. By restricting access to critical files through IIS rules and temporarily granting write permissions only when needed, this approach helps mitigate the risks of exposing WordPress to potential security threats.

Disclaimer

This solution represents my best effort to address the file permission issues for WordPress on IIS using Windows. While it has worked in my environment and may be effective for others, I cannot make any promises about its efficacy in every case. Use this solution at your own risk and always test thoroughly in your environment.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *