Protecting Singular Paths with .htaccess

Introduction

In this guide, we demonstrate how to protect singular paths (for example, a single file like wp-login.php or a single folder like /admin) using a PHP script that updates your .htaccess. We’ll show:

  1. Scenario A – Protecting a single file (like wp-login.php) using the original <Files wp-login.php> ... </Files> approach.
  2. Scenario B – Protecting a single folder (like /admin) with IP-based restrictions using an Apache <If> directive in .htaccess.

Scenario A: Protecting a Single File (wp-login.php)

Below is the original script you provided, which demonstrates how to protect wp-login.php by editing the <Files wp-login.php> block in .htaccess. It also includes a simple PHP-based login to limit access to the editor page itself.

1. .htaccess Block

Your .htaccess file should have a section like:

<Files wp-login.php>
Order Deny,Allow
Deny from all
Allow from 1.2.3.4
</Files>
  • Deny from all blocks everyone.
  • Allow from 1.2.3.4 allows IP 1.2.3.4.
  • This ensures only whitelisted IPs can reach wp-login.php.

2. PHP Script

Below is the complete PHP code. Copy/paste it into a file (e.g., manage_wp_login_ips.php) on your server. Adjust $STATIC_PASSWORD_HASH to your password hash, and $htaccessPath to the correct path to .htaccess.

<?php

session_start();

/**************************************************************
 *                   CONFIGURATION
 **************************************************************/
// Store the hashed password here (Replace this with your own!)
$STATIC_PASSWORD_HASH = '$2y$10$Lla9zPARA7Dh5siAQZFRE.mWRjr9IgHcaab6lDzm4Kz4qd.kTfwiC';

// Path to the .htaccess file in your public_html folder
$htaccessPath = '/home/store18/public_html/.htaccess';  // <-- Adjust this path

/**************************************************************
 *             SIMPLE PASSWORD PROTECTION CHECK
 **************************************************************/
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
    // User is not authenticated, check if they provided a password
    if (isset($_POST['password'])) {
        // Use password_verify() instead of direct string comparison
        if (password_verify($_POST['password'], $STATIC_PASSWORD_HASH)) {
            $_SESSION['authenticated'] = true;
            header("Location: " . $_SERVER['PHP_SELF']);
            exit;
        } else {
            $loginError = "Invalid password.";
        }
    }

    // Display a simple login form (Bootstrap 5)
    ?>
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <title>Login Required</title>
      <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    </head>
    <body class="bg-light">
      <div class="container py-5">
        <div class="row justify-content-center">
          <div class="col-12 col-md-6 col-lg-4">
            <div class="card">
              <div class="card-body">
                <h1 class="h5 text-center mb-4">Protected Access</h1>
                <?php if (!empty($loginError)): ?>
                  <div class="alert alert-danger"><?php echo htmlspecialchars($loginError); ?></div>
                <?php endif; ?>
                <form method="post">
                  <div class="mb-3">
                    <label for="password" class="form-label">Password</label>
                    <input type="password" name="password" id="password" class="form-control" required>
                  </div>
                  <button type="submit" class="btn btn-primary w-100">Login</button>
                </form>
              </div>
            </div>  
          </div>
        </div>
      </div>
    </body>
    </html>
    <?php
    exit;
}
/**************************************************************
 *           END: SIMPLE PASSWORD PROTECTION
 **************************************************************/

/**************************************************************
 *  HELPER FUNCTIONS TO READ & UPDATE THE .HTACCESS BLOCK
 **************************************************************/

/**
 * Extract IP lines from the <Files wp-login.php> block.
 *
 * @param string $content  Entire contents of .htaccess
 * @return array [ 'startIndex' => int, 'endIndex' => int, 'ips' => string[] ]
 */
function parseWpLoginBlock($content) {
    $lines = explode("\n", $content);

    $startIndex = -1;
    $endIndex   = -1;
    $ips        = [];

    // find <Files wp-login.php> start and </Files> end
    for ($i = 0; $i < count($lines); $i++) {
        $trimmed = trim($lines[$i]);
        if (stripos($trimmed, '<Files wp-login.php>') !== false) {
            $startIndex = $i;
        }
        if (stripos($trimmed, '</Files>') !== false && $startIndex !== -1 && $endIndex === -1) {
            $endIndex = $i;
            break;
        }
    }

    // If valid start/end, parse lines in between
    if ($startIndex !== -1 && $endIndex !== -1) {
        for ($j = $startIndex; $j <= $endIndex; $j++) {
            if (stripos(trim($lines[$j]), 'Allow from') === 0) {
                // e.g. "Allow from 79.119.99.83"
                $parts = preg_split('/\s+/', trim($lines[$j]));
                // $parts[0] = "Allow", $parts[1] = "from", $parts[2] = "x.x.x.x"
                if (isset($parts[2])) {
                    $ips[] = $parts[2];
                }
            }
        }
    }

    return [
        'startIndex' => $startIndex,
        'endIndex'   => $endIndex,
        'ips'        => $ips
    ];
}

/**
 * Replace the IPs in the <Files wp-login.php> block with new IPs.
 *
 * @param string $content   Entire .htaccess content
 * @param array  $newIPs    Array of new IP strings
 * @return string           Updated .htaccess content
 */
function updateWpLoginBlock($content, $newIPs) {
    $lines = explode("\n", $content);
    $blockInfo = parseWpLoginBlock($content);
    $startIndex = $blockInfo['startIndex'];
    $endIndex   = $blockInfo['endIndex'];

    // If we cannot find the block, do nothing
    if ($startIndex === -1 || $endIndex === -1) {
        return $content;
    }

    // Rebuild the block lines
    $rebuiltBlock = [];
    for ($i = $startIndex; $i <= $endIndex; $i++) {
        // Keep lines that are not "Allow from ...", skip the old IP lines
        $trimmed = trim($lines[$i]);
        if (stripos($trimmed, 'Allow from') === 0) {
            continue;
        }
        $rebuiltBlock[] = $lines[$i];

        // Right after 'Deny from all', re-inject new "Allow from" lines
        if (stripos($trimmed, 'Deny from all') === 0) {
            foreach ($newIPs as $ip) {
                $rebuiltBlock[] = "Allow from " . $ip;
            }
        }
    }

    // Replace lines in $lines
    $blockLength = ($endIndex - $startIndex) + 1;
    array_splice($lines, $startIndex, $blockLength, $rebuiltBlock);

    return implode("\n", $lines);
}

/**************************************************************
 *    PROCESS FORM SUBMISSION (SAVE NEW IP ADDRESSES)
 **************************************************************/
if (isset($_POST['save_ips']) && isset($_POST['ips'])) {
    // Clean the posted IPs, remove empty lines, etc.
    $newIPs = array_map('trim', $_POST['ips']);
    $newIPs = array_filter($newIPs, fn($ip) => $ip !== '');

    // Read the .htaccess file content
    if (!file_exists($htaccessPath)) {
        die("Error: .htaccess not found at $htaccessPath");
    }
    $htaccessContent = file_get_contents($htaccessPath);

    // Update the wp-login.php block
    $updatedContent = updateWpLoginBlock($htaccessContent, $newIPs);

    // Save changes
    file_put_contents($htaccessPath, $updatedContent);

    // Redirect or confirm success
    header("Location: " . $_SERVER['PHP_SELF'] . "?updated=1");
    exit;
}

/**************************************************************
 *        READ CURRENT IPs AND DISPLAY HTML FORM
 **************************************************************/
$htaccessContent = file_get_contents($htaccessPath);
$blockInfo = parseWpLoginBlock($htaccessContent);
$currentIPs = $blockInfo['ips'];

?>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Manage WordPress Login IPs</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-4">
  <h1 class="mb-4">Manage Allowed IPs for <code>wp-login.php</code></h1>

  <?php if (isset($_GET['updated'])): ?>
    <div class="alert alert-success">IP addresses have been updated successfully!</div>
  <?php endif; ?>

  <form method="post">
    <input type="hidden" name="save_ips" value="1" />

    <table class="table table-bordered bg-white">
      <thead>
        <tr>
          <th>#</th>
          <th>IP Address</th>
        </tr>
      </thead>
      <tbody>
      <?php 
        if (!empty($currentIPs)) {
          foreach ($currentIPs as $index => $ip) {
      ?>
              <tr>
                <td><?php echo ($index + 1); ?></td>
                <td>
                  <input type="text" name="ips[]" value="<?php echo htmlspecialchars($ip); ?>" class="form-control">
                </td>
              </tr>
      <?php
          }
        } else {
      ?>
          <tr>
            <td>1</td>
            <td>
              <input type="text" name="ips[]" value="" class="form-control" placeholder="e.g. 1.2.3.4">
            </td>
          </tr>
      <?php
        }
      ?>
      </tbody>
    </table>

    <div class="mb-3">
      <button type="button" class="btn btn-secondary" onclick="addRow()">+ Add another IP</button>
    </div>

    <button type="submit" class="btn btn-primary">Save IPs</button>
  </form>
</div>

<script>
function addRow() {
    const tableBody = document.querySelector('table tbody');
    const rowCount = tableBody.rows.length;
    const newRow = tableBody.insertRow();

    let cellIndex = newRow.insertCell(0);
    cellIndex.innerHTML = rowCount + 1;

    let cellIP = newRow.insertCell(1);
    cellIP.innerHTML = '<input type="text" name="ips[]" value="" class="form-control" placeholder="e.g. 1.2.3.4">';
}
</script>

</body>
</html>
  1. Upload this file to your server.
  2. Access it in your browser, log in with your set password, and add IP addresses.
  3. The script updates <Files wp-login.php> in .htaccess.

That’s how you protect a single file (wp-login.php) with your custom IP-based restrictions.


Scenario B: Protecting a Single Folder (/admin)

Now, if you want to protect an entire folder like /admin, <Files wp-login.php> won’t help, because <Files> only applies to specific filenames. For a folder, you must use different Apache directives.

1. Using <If> in .htaccess (Apache 2.4+)

If your Apache version supports the <If> directive in .htaccess, you can do something like:

<If "%{REQUEST_URI} =~ m#^/admin/#">
    Order Deny,Allow
    Deny from all
    Allow from 1.2.3.4
</If>
  • This says: if the incoming URL path starts with /admin/, deny all except the listed IP(s).
  • You can add more Allow from X.X.X.X lines as needed.

2. Parsing & Updating the <If> Block

Your PHP script logic becomes similar to Scenario A, but you search for <If "%{REQUEST_URI} =~ m#^/admin/#"> and </If> lines, and insert or remove Allow from ... lines inside that block. For example, replace the parseWpLoginBlock() logic with something like parseAdminIfBlock(), searching for:

  • <If "%{REQUEST_URI} =~ m#^/admin/#">
  • </If>

And the update logic (updateWpLoginBlock()) becomes updateAdminIfBlock() that re-injects new Allow from lines after Deny from all.

Below is a simplified example (not a full final script) to illustrate:

function parseAdminIfBlock($content) {
    $lines = explode("\n", $content);
    $startIndex = -1;
    $endIndex   = -1;
    $ips        = [];

    for ($i = 0; $i < count($lines); $i++) {
        $trimmed = trim($lines[$i]);
        if (stripos($trimmed, '<If "') !== false 
            && stripos($trimmed, 'REQUEST_URI') !== false
            && stripos($trimmed, '/admin/') !== false) {
            $startIndex = $i;
        }
        if (stripos($trimmed, '</If>') !== false && $startIndex !== -1 && $endIndex === -1) {
            $endIndex = $i;
            break;
        }
    }

    if ($startIndex !== -1 && $endIndex !== -1) {
        for ($j = $startIndex; $j <= $endIndex; $j++) {
            if (stripos(trim($lines[$j]), 'Allow from') === 0) {
                $parts = preg_split('/\s+/', trim($lines[$j]));
                if (isset($parts[2])) {
                    $ips[] = $parts[2];
                }
            }
        }
    }

    return [
        'startIndex' => $startIndex,
        'endIndex'   => $endIndex,
        'ips'        => $ips
    ];
}

function updateAdminIfBlock($content, $newIPs) {
    $lines = explode("\n", $content);
    $blockInfo = parseAdminIfBlock($content);
    $startIndex = $blockInfo['startIndex'];
    $endIndex   = $blockInfo['endIndex'];

    if ($startIndex === -1 || $endIndex === -1) {
        return $content;
    }

    $rebuiltBlock = [];
    for ($i = $startIndex; $i <= $endIndex; $i++) {
        $trimmed = trim($lines[$i]);
        // Skip old "Allow from" lines
        if (stripos($trimmed, 'Allow from') === 0) {
            continue;
        }
        $rebuiltBlock[] = $lines[$i];

        // Re-inject "Allow from" lines after "Deny from all"
        if (stripos($trimmed, 'Deny from all') === 0) {
            foreach ($newIPs as $ip) {
                $rebuiltBlock[] = "Allow from " . $ip;
            }
        }
    }

    // Replace the lines in the main array
    $blockLength = ($endIndex - $startIndex) + 1;
    array_splice($lines, $startIndex, $blockLength, $rebuiltBlock);

    return implode("\n", $lines);
}

Then you’d hook these functions into the same password-protected editor form that Scenario A used, but now referencing <If> blocks for /admin instead of <Files wp-login.php>.

3. Other Methods for Folders

  • <Directory> blocks in the main Apache config. This is typically done if you control the server (VPS/Dedicated). Example:
    <Directory "/var/www/html/admin">
      Require ip 1.2.3.4
    </Directory>
  • .htpasswd BasicAuth is also an option, but that’s a different approach (username/password, not IP-based).

Conclusion

  1. Scenario A (single file protection) uses <Files wp-login.php> or <FilesMatch> in .htaccess.
  2. Scenario B (single folder protection) needs either <If> (Apache 2.4+), <Directory> in the main config, or a different approach.

Your PHP script can be adapted in both cases by changing the block it searches for:

  • <Files wp-login.php> vs. <If "...">
  • And the start/end markers (</Files> vs. </If>).

All the rest (reading IPs, rewriting lines, injecting Allow from) follows the same pattern as the original script.

This process empowers you to manage IP restrictions for singular paths—either a single file (wp-login.php) or a single folder (/admin)—from your PHP-based interface!