.htaccess
Many PHP applications ship with an .htaccess file: WordPress, Laravel, ready-made HTTPS and www redirects, access rules, password-protected folders. WardPane reads and applies these files itself, so the usual files work as they are without Apache on the server.
Turning it on
Open the site, go to Advanced and turn on .htaccess under “Apache compatibility”. It is off by default.
.htaccess processing is part of the PHP handler. A static site that had it on before 2.0 keeps it, new static sites do not process .htaccess files, and a Node.js app handles its own routes.
When it is on, .htaccess files in the document root and in every subfolder are applied the way Apache applies them: a subfolder with its own .htaccess does not inherit the RewriteRules of its parent unless it says RewriteOptions Inherit.
Supported directives
| Area | Directives |
|---|---|
| URL rewriting | RewriteEngine, RewriteBase, RewriteCond, RewriteRule, RewriteOptions Inherit. Flags L, END, R=code (300 to 599; 4xx and 5xx answer directly without a redirect), F, G, NC, QSA, QSD, NE, C, S, B, T=type (an ordinary content type only, not PHP or CGI). Tests -f, -d, -s, -l, =, <, >, -eq and the like, -ipmatch. Variables %{HTTP_HOST}, %{HTTPS}, %{REQUEST_URI}, %{REQUEST_FILENAME}, %{QUERY_STRING}, %{HTTP:Header} and others. \x in a substitution is a literal x. |
| Redirects | Redirect, RedirectPermanent, RedirectTemp, RedirectMatch, Redirect gone. Codes 300 to 599; outside 3xx the code is the answer itself, without a location (for example RedirectMatch 403 ... from the 5G blacklist). |
| Access | Require all/ip/not ip/local/method/valid-user/user, <RequireAll>, <RequireAny>, <RequireNone>, Order, Allow from, Deny from, Satisfy |
| Passwords | AuthType Basic, AuthName, AuthUserFile (bcrypt from htpasswd -B, the default MD5, {SHA}) |
| Responses | ErrorDocument, Header set/append/add/merge/unset/setifempty [always], ExpiresActive, ExpiresDefault, ExpiresByType, AddType, AddDefaultCharset, DirectoryIndex |
| Blocks | <Files>, <FilesMatch>, <Limit>, <LimitExcept>, <IfModule>, <IfVersion> |
Details that behave like Apache
<IfModule mod_php...>and<IfModule mod_deflate...>count as missing modules: their content is skipped, as on a server without them.%{REQUEST_FILENAME}is the path up to the first existing file, the rest isPATH_INFO./index.php/2024/helloisindex.php, so!-fis false and WordPress does not rewrite the address.Expires...leaves error responses alone, and responses that already have anExpiresheader (set by a script). Itsmax-agereplaces the panel’s defaultCache-Controlfor static files (public, max-age=0) and is added to one set by a script or byHeader(Laravel’sno-cache, privatestays).\in an argument is a space inside a word, as inRewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index\.php\ HTTP/.Options +Indexesgives a warning: there are no directory listings, and a folder without an index page answers 404.
What is refused
Directives that would change what gets executed or weaken the PHP sandbox are refused:
php_value,php_flagAddHandler,SetHandler,ForceType,ActionAddTypefor PHP or CGI
.htaccess files in folders that PHP can write to are ignored, because a hacked script could otherwise rewrite the rules that protect the site.
Security behaviour
Password files
The password file named in AuthUserFile must be inside the site folder. A relative path is taken from the site folder. Inside the document root only a file whose name starts with a dot (.htpasswd) is accepted, since anything else could be downloaded; a folder that points to such a file does not open (500).
Password checks are protected against guessing:
- after 10 wrong passwords in 10 minutes, an address (for IPv6, its
/64) is not checked for 15 minutes; - after 300 wrong passwords from all addresses, the site goes into attack mode for 5 minutes: passwords are checked only for addresses without recent failures, so a botnet gets one try per address while someone with the right password still gets in;
- successful checks are cached, because the browser sends the password with every request and bcrypt is slow on purpose; identical checks running at the same time are done once;
- bcrypt runs in separate threads and does not slow down other sites: at most 4 checks at a time per site and 32 in the queue, with a wait of up to 10 seconds, after which the answer is 503 (not another password prompt);
- the bcrypt cost may be at most 12 (
htpasswd -B -C 12); more expensive hashes are rejected; - a failed check never answers faster than 300 ms, so a missing user cannot be told from a wrong password by timing.
Paths and file blocks
- Addresses with
.or..segments (also as%2e) get 400. Otherwise/./admin/could slip past the.htaccessof theadminfolder. <Files>and<FilesMatch>are compared with the file that will really be served: for/admin.php/xthat isadmin.php, whatever the last segment of the address.Headercannot change headers that control the connection:Content-Length,Transfer-Encoding,Connection,Upgrade,Alt-Svcand similar.
Broken files fail closed
An .htaccess that cannot be read or parsed, or that is larger than 256 KB, closes its folder: every request to that folder gets 500 until the file is fixed, and an error is written to the log. Apache does the same. If the folder stayed open, the access rules in that file would silently stop working.
An error in a single line does not close the folder. Such errors are handled so that a mistake never opens access:
- An error in an address list in
Allow,DenyorRequire ipis not forgiven, but it affects only its own section and does not cause a 500. An unrecognised entry inDeny frommeans “deny from all” in that section; inAllow fromorRequire ipit means “nobody”. A host name instead of an address never matches (with a warning); garbage is an error in the report.localhostis understood (127.0.0.1and::1), and/0means all addresses. - A section whose condition cannot be checked (
<Files>or<FilesMatch>with an unreadable pattern,<If>) keeps only its access rules, and those can only restrict: the stricter of the decisions with and without them wins. - An unreadable
RewriteCondalso disables theRewriteRuleafter it, which would otherwise fire where it should not.[S=n]counts and[C]chains stay correct. The exception is a rule that only denies ([F],[G],[R=4xx]): it stays, without its conditions, and denies everything its pattern matches. IfRewriteBasecannot be read, rules with a relative target are not applied.
Limits on regular expressions
Patterns from .htaccess cannot stop the server, and the panel’s own limits never take a whole site down: when a limit is hit, only that one request fails.
How patterns run:
- A pattern is never rejected for what it might cost.
- Patterns that a linear-time engine can handle run right in the request, as long as their estimated cost fits the budget. Very large ones (say, a list of 2000 bot names) go to a separate worker thread.
- A repeated character class like
([a-z0-9]{2000,})from the 7G and 6G firewalls, or\d{20,}, is checked in the request with a linear scan. - Everything else JavaScript understands (lookahead, large counters, back references, nested repeats) runs in the worker thread.
- Only syntax that exists in PCRE alone is rejected, and that line is not applied:
\K,\G,\Q...\E, possessivea++, atomic groups(?>...), recursion(?R), comments(?#...), negated POSIX classes[[:^alpha:]]and a few other escapes. Common PCRE forms are translated:\A,\z,\Z,\eand POSIX classes such as[[:alpha:]]work.
The limits:
| Limit | Value | What happens |
|---|---|---|
| One pattern in the worker thread | 25 ms | 500 for this request only; the pattern stays in use |
| All patterns of one request | 50 ms | 503 with Retry-After |
| Patterns per file | 500 | |
Size of one .htaccess |
256 KB | The folder answers 500 until the file is fixed |
| Waiting for a free worker | 20 ms | 503 with Retry-After: 1 |
Strings that patterns are matched against are capped too: the address at 8190 characters, headers at 2 KB, Cookie at 8 KB, file names at 255. Longer strings get 414 (431 for a header), but only when a pattern is actually applied to them. A string too long for a big pattern (a 2 KB User-Agent against a list of 2000 bots) gets 431 or 414 at once.
Slow pattern runs are charged to the visitor who caused them. Only timeouts and slow backtracking patterns count against a visitor (an address, for IPv6 its /64, with a separate count per /48); ordinary checks are free, so people behind a shared NAT do not suffer. Whoever causes 5 timeouts in 10 minutes or uses up their budget gets 429 on that site for 10 minutes and is handed to the site’s protection (with DDoS protection on, that means a ban). The site allow list and shared Cloudflare nodes are never punished.
A big bot list costs every request a few milliseconds (2000 names take about 4 ms for a Chrome request). Turn on DDoS protection for such a site to keep floods of requests away.
Check .htaccess files
The panel does not guess at lines it does not understand: it skips them and reports them. Press Check .htaccess files on the Advanced tab to see every .htaccess of the site and the lines that are not applied, each with the reason. When a file changes, a warning about lines that are not applied is also written to the panel log.
Not supported
<If>and<Else>(with a warning; only access rules are kept from their content, and they work against access whatever the condition)- directory listings (
Options +Indexes) - environment variables:
SetEnv,SetEnvIf,E=,env= - compression (
mod_deflate) ErrorDocumentpointing to a PHP script- checks by host name (
Require hostand other unknownRequireforms count against access; for names inAllowandDeny, see above) - the
RewriteRuleflagsP(proxy),NandH