Dokumentacja jest dostępna po angielsku i po rosyjsku. To jest wersja angielska.

.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 is PATH_INFO. /index.php/2024/hello is index.php, so !-f is false and WordPress does not rewrite the address.
  • Expires... leaves error responses alone, and responses that already have an Expires header (set by a script). Its max-age replaces the panel’s default Cache-Control for static files (public, max-age=0) and is added to one set by a script or by Header (Laravel’s no-cache, private stays).
  • \ in an argument is a space inside a word, as in RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index\.php\ HTTP/.
  • Options +Indexes gives 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_flag
  • AddHandler, SetHandler, ForceType, Action
  • AddType for 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 .htaccess of the admin folder.
  • <Files> and <FilesMatch> are compared with the file that will really be served: for /admin.php/x that is admin.php, whatever the last segment of the address.
  • Header cannot change headers that control the connection: Content-Length, Transfer-Encoding, Connection, Upgrade, Alt-Svc and 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, Deny or Require ip is not forgiven, but it affects only its own section and does not cause a 500. An unrecognised entry in Deny from means “deny from all” in that section; in Allow from or Require ip it means “nobody”. A host name instead of an address never matches (with a warning); garbage is an error in the report. localhost is understood (127.0.0.1 and ::1), and /0 means 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 RewriteCond also disables the RewriteRule after 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. If RewriteBase cannot 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, possessive a++, atomic groups (?>...), recursion (?R), comments (?#...), negated POSIX classes [[:^alpha:]] and a few other escapes. Common PCRE forms are translated: \A, \z, \Z, \e and 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)
  • ErrorDocument pointing to a PHP script
  • checks by host name (Require host and other unknown Require forms count against access; for names in Allow and Deny, see above)
  • the RewriteRule flags P (proxy), N and H