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

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:
- 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.
- The
- Blocking Root PHP Files Except for Key Exceptions:
- Certain WordPress PHP files in the root directory (such as
index.php
,wp-login.php
, andwp-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
- Certain WordPress PHP files in the root directory (such as
- Blocking Dangerous HTTP Methods:
- HTTP methods such as
PUT
,DELETE
, andPROPFIND
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.
- HTTP methods such as
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.
Leave a Reply