4. RewriteRule¶
Quickly, bring me a beaker of wine, that I may wetmy brain and say something clever.—Aristophanes (attributed)
I’ll start the main technical discussion of mod_rewrite with the
RewriteRule directive, as it is the workhorse of mod_rewrite, and the
directive that you’ll encounter most frequently.
RewriteRule performs manipulation of a requested URL, and along the way
can do a number of additional things. It’s where the actual rewriting
happens — everything else in mod_rewrite exists to support it.
The syntax of a RewriteRule is fairly simple, but you’ll find that exploring all of the possible permutations of it will take a while. So I’ll provide a lot of examples along the way to illustrate.
If you learn best by example, you may want to jump back and forth between this section and Recipes to help you make sense of this all.
4.1. Syntax¶
A RewriteRule directive has two required arguments and optional flags. It looks like:
RewriteRule PATTERN TARGET [FLAGS]
The following sections will discuss each of those arguments in great detail, but these are defined as:
- PATTERN
A regular expression to be applied to the requested URI.
- TARGET
What the URI will be rewritten to.
- FLAGS
Optional flags that modify the behavior of the rule.
4.2. Pattern¶
The PATTERN argument of the RewriteRule is a regular expression that
is applied to the URL path, or file path, depending on the context.
In VirtualHost context, or in server-wide context, PATTERN will be
matched against the part of the URL after the hostname and port, and
before the query string (the %-decoded URL-path). For example, in the URL
<http://example.com/dogs/index.html?dog=collie>, the pattern will be
matched against /dogs/index.html.
In per-directory context — that is, within a <Directory>,
<DirectoryMatch>, <Files>, or <FilesMatch> section, or in a
.htaccess file — PATTERN will be matched against the
filesystem path, after removing the prefix that led the server to the
current RewriteRule (e.g. either “dogs/index.html” or “index.html”
depending on where the directives are defined). See
Per-directory context gotchas below for the gory details of how this
prefix stripping works — it’s one of the most common sources of
confusion.
Subsequent RewriteRule patterns are matched against the output of the
last matching RewriteRule.
It is assumed, at this point, that you’ve already read the chapter Introduction to Regular Expressions, and/or are familiar with what a regular expression is, and how to craft one.
4.2.1. Negated patterns¶
You can prefix the pattern with an exclamation mark (!) to negate
it. This means the rule fires when the URL does not match the
pattern. I find this useful for “everything except” rules — for
example, redirecting all requests that are not for a specific path:
# Redirect everything that ISN'T the maintenance page
RewriteRule !^maintenance\.html$ /maintenance.html [R=302,L]
There’s one important caveat: when you negate a pattern, there’s nothing
to capture. The pattern didn’t match, so there are no groups, and
$1, $2, etc. are empty. If you need backreferences in the
target and you need a negated match, use a RewriteCond instead:
# This does NOT work — $1 is empty because the pattern is negated
RewriteRule !^secret/ /public/$1 [L]
# Do this instead
RewriteCond %{REQUEST_URI} !^/secret/
RewriteRule ^(.*)$ /public/$1 [L]
See RewriteCond for more on conditions.
4.3. Target¶
The target of a RewriteRule can be one of the following:
4.3.1. A file-system path¶
Designates the location on the file-system of the resource to be delivered to the client. Substitutions are only treated as a file-system path when the rule is configured in server (virtualhost) context and the first component of the path in the substitution exists in the file-system
4.3.2. URL-path¶
A DocumentRoot-relative path to the resource to be served. Note that
mod_rewrite tries to guess whether you have specified a file-system path
or a URL-path by checking to see if the first segment of the path exists
at the root of the file-system. For example, if you specify a
Substitution string of /www/file.html, then this will be treated as a
URL-path unless a directory named www exists at the root or your
file-system (or, in the case of using rewrites in a .htaccess file,
relative to your document root), in which case it will be treated as a
file-system path. If you wish other URL-mapping directives (such as
Alias) to be applied to the resulting URL-path, use the [PT] flag as
described below.
4.3.3. Absolute URL¶
If an absolute URL is specified, mod_rewrite checks to see whether the
hostname matches the current host. If it does, the scheme and hostname
are stripped out and the resulting path is treated as a URL-path.
Otherwise, an external redirect is performed for the given URL. To force
an external redirect back to the current host, see the [R] flag below.
4.3.4. - (dash)¶
A dash indicates that no substitution should be performed (the existing path is passed through untouched). This is used when a flag (see below) needs to be applied without changing the path.
For example, to set an environment variable without rewriting the URL:
RewriteRule ^/secret - [E=NEED_AUTH:1]
4.4. Backreferences¶
If the Pattern section was the “input” side of RewriteRule, backreferences
are where things get interesting on the “output” side. Any parenthesized
group in the pattern creates a backreference that you can use in the
target string. These are numbered $1 through $9, left to right,
by opening parenthesis.
# Request: /products/widgets/42
RewriteRule ^/products/([^/]+)/([0-9]+)$ /catalog.php?category=$1&id=$2 [L]
# Result: /catalog.php?category=widgets&id=42
Here, $1 captures widgets and $2 captures 42. The
numbering follows the same rules as PCRE backreferences, which I
covered in Regular Expressions.
You can also use backreferences from RewriteCond patterns in the
target. These use the %N syntax (%1 through %9) rather than
$N, which helps you tell at a glance which part of the rule
generated a particular capture:
RewriteCond %{HTTP_HOST} ^([^.]+)\.example\.com$
RewriteRule ^/(.*)$ /sites/%1/$1 [L]
In that example, %1 is the subdomain captured by the
RewriteCond, and $1 is the path captured by the
RewriteRule. A request for http://blog.example.com/hello
becomes /sites/blog/hello.
Roy Fielding’s original design for HTTP kept the URL opaque to the
server — just a string to be resolved. mod_rewrite cheerfully
violates that principle by tearing URLs apart and reassembling them from
captured pieces. It’s tremendously useful, but do keep in mind that
you’re working against the grain of the protocol’s architecture every
time you do it.
4.4.1. Server variables in the target¶
In addition to backreferences, the target string can contain server
variables using the %{VARNAME} syntax — the same variables
available in RewriteCond (see RewriteCond).
# Redirect HTTP to HTTPS, preserving the host and path
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
Common variables you’ll use in targets include %{HTTP_HOST},
%{SERVER_PORT}, %{REQUEST_URI}, and %{QUERY_STRING}.
You can also reference RewriteMap functions in the target
using the ${mapname:key|default} syntax. I’ll cover that in detail
in RewriteMap.
The order in which these are expanded matters: backreferences ($N
and %N) are expanded first, then server variables (%{VARNAME}),
then map function calls (${mapname:...}). In practice this means
you can use a backreference inside a map lookup key, which is exactly
how dynamic RewriteMap-based routing works.
4.5. Query string handling¶
This trips up almost everyone the first time: the query string is not
part of the pattern match. If a user requests
/search?q=kittens, the pattern only sees /search. The query
string passes through to the rewritten URL unchanged, silently, behind
your back.
That’s usually what you want. But when it isn’t, here’s how to take control:
Replacing the query string — put a ? in the target. Everything
after it becomes the new query string, and the old one is discarded:
# /old-search?q=kittens → /new-search?type=cat
# (the original ?q=kittens is thrown away)
RewriteRule ^/old-search$ /new-search?type=cat [L]
Erasing the query string — end the target with a bare ?:
# /page?tracking=utm_garbage → /page (clean)
RewriteRule ^/page$ /page? [L]
Appending to the existing query string — use the [QSA]
(Query String Append) flag:
# /products/widgets → /catalog.php?category=widgets&q=kittens
# (preserves the original query string)
RewriteRule ^/products/(.+)$ /catalog.php?category=$1 [QSA,L]
Discarding the query string explicitly — use the [QSD] flag
(available since httpd 2.4.0):
RewriteRule ^/clean-path$ /target [QSD,L]
There’s also [QSL] (Query String Last), which changes how
mod_rewrite identifies the split between the path and the query
string when the target itself contains a literal ?. See
RewriteRule Flags for the full details on all of these.
4.6. Per-directory context gotchas¶
I mentioned earlier that in per-directory context (<Directory>
blocks and .htaccess files), the directory prefix is stripped
before matching. Let me be more specific, because this is where I see
the most head-scratching on Stack Overflow and the httpd users mailing
list.
The stripped prefix always ends with a slash. So if your rules live
in /var/www/html/.htaccess and someone requests
/images/logo.png, the pattern sees images/logo.png — no
leading slash. This means a pattern that starts with ^/ will
never match in per-directory context:
# In .htaccess — this NEVER matches
RewriteRule ^/images/(.*)$ /img/$1 [L]
# This is what you want
RewriteRule ^images/(.*)$ /img/$1 [L]
If you need to match against the full original URL-path from within a
.htaccess file, use a RewriteCond with %{REQUEST_URI}:
RewriteCond %{REQUEST_URI} ^/images/(.*)$
RewriteRule ^ /img/%1 [L]
One more thing: although RewriteRule is syntactically valid inside
<Location>, <Files>, and <If> blocks, this
is unsupported and you should not do it. Relative substitutions in
particular will break in creative and frustrating ways. Stick to
<Directory>, <VirtualHost>, server config, and
.htaccess.
Warning
<If> silently switches to per-directory context
Placing a RewriteRule inside an <If> block — even when that
<If> is nested inside a <VirtualHost> — silently switches
the rule to per-directory context behavior. This means:
The leading slash is stripped from the URL before pattern matching.
Substitutions trigger an internal redirect and re-entry (loop risk).
[L]no longer truly stops processing — you need[END].
The same applies to <Location> and <Files> blocks. If your
rules are doing nothing or looping unexpectedly, check whether
they’re wrapped in one of these containers.
Prefer placing rewrite rules directly in the <VirtualHost> or
server-level context where they run in the URL-to-filename
translation phase, with full URL-path matching and no re-entry
behavior.
4.7. Home directory expansion¶
Here’s an obscure one that has bitten a few people: when the target
string begins with something that looks like /~user (whether from
literal text or from a backreference), mod_rewrite performs home
directory expansion automatically — even if mod_userdir is not
loaded or configured. This happens because the expansion is built into
mod_rewrite itself.
If this behavior surprises you (and it will, the first time it bites),
you can suppress it with the [PT] (passthrough) flag, which hands
the rewritten URL back to the normal URL mapping pipeline rather than
letting mod_rewrite resolve it directly.
4.8. How rules are processed¶
RewriteRules in a given context are processed in order, top to bottom. Each rule’s pattern is matched against the result of the previous matching rule — not against the original request. This is important:
RewriteRule ^/dogs/(.*)$ /pets/$1 [L]
RewriteRule ^/pets/(.*)$ /animals/$1 [L]
A request for /dogs/fido matches the first rule and is rewritten to
/pets/fido. But the [L] flag stops processing, so the second
rule never fires. Without the [L], the second rule would match
the output of the first — /pets/fido — and rewrite it to
/animals/fido. This cascading behavior is powerful but can create
unintentional loops if you’re not careful. See
RewriteRule Flags for more on [L], [END], and
other flags that control the processing flow.
When RewriteCond directives precede a rule, the engine evaluates
them only after the pattern matches — despite the fact that they appear
before the rule in the config file. If any condition fails, the rule
is skipped entirely. This is covered in detail in
RewriteCond.
4.9. Flags at a glance¶
Flags are the third argument to RewriteRule and modify its behavior
in various ways. I cover each flag in detail in
RewriteRule Flags, but here’s a quick reference so you
can orient yourself:
Flag |
Purpose |
|---|---|
|
Escape backreferences before applying them |
|
Chain this rule to the next rule |
|
Set a cookie |
|
Discard path info |
|
Set an environment variable |
|
Stop processing and don’t re-run in per-directory context |
|
Return 403 Forbidden |
|
Return 410 Gone |
|
Force a content handler |
|
Last rule — stop processing this ruleset |
|
Re-run from the top (next round) |
|
Case-insensitive match |
|
Don’t escape special characters in the output |
|
Skip if this is an internal sub-request |
|
Proxy the request |
|
Pass through to the next URL mapping handler |
|
Append the original query string |
|
Discard the original query string |
|
Use the last |
|
External redirect (optionally with status code) |
|
Skip the next N rules |
|
Set the MIME type |
4.10. Security Considerations¶
RewriteRule is a powerful URL manipulation tool, and with that power
comes the potential for security mistakes. The following pitfalls are
worth keeping in mind whenever you write rules that incorporate
user-controlled input — backreferences from the URL, query string
values, or HTTP headers.
4.10.1. Open Redirects¶
If a RewriteRule constructs a redirect URL using unvalidated user
input, an attacker can craft a link that redirects visitors to a
malicious site while appearing to originate from your domain. This is
known as an open redirect vulnerability.
# DANGEROUS — allows open redirect
RewriteRule ^/redirect %{QUERY_STRING} [R,L]
An attacker could use
https://yoursite.com/redirect?https://evil.com to redirect users to
a malicious site with your domain in the address bar during the click.
Mitigation: Always validate or constrain redirect targets. If the
destination must be on your own site, ensure the substitution begins
with / (a relative path) rather than allowing a full URL from user
input. Or validate against a whitelist of allowed domains.
4.10.2. Server-Side Request Forgery (SSRF)¶
When using the [P] (proxy) flag, mod_rewrite causes the
server to make an HTTP request to the substitution URL on behalf of the
client. If any part of that URL is derived from user input —
backreferences, query strings, or headers — an attacker may be able to
cause your server to make requests to arbitrary internal services or
external hosts.
# DANGEROUS — user controls the proxy target
RewriteCond %{QUERY_STRING} target=(.+)
RewriteRule ^/fetch http://%1 [P]
An attacker could use this to probe internal network services
(http://169.254.169.254/latest/meta-data/ on EC2, for instance)
that are not accessible from the internet.
Mitigation: Always use a fixed hostname in proxy targets. Limit
backreferences to the path component only, and validate that captured
values cannot contain :// or other scheme indicators.
4.10.3. Path Traversal¶
Rules that map user-supplied path components directly to the filesystem can allow path traversal attacks if the input is not properly constrained:
# DANGEROUS — allows path traversal
RewriteRule ^/files/(.+) /var/data/$1 [L]
A request for /files/../../etc/passwd could potentially access files
outside the intended directory (although Apache’s <Directory>
restrictions and Options settings provide defense in depth).
Mitigation: Use restrictive patterns — [a-zA-Z0-9_.-]+ instead
of .+ — and rely on Apache’s built-in protections as additional
layers. Never assume the regex alone will prevent abuse.
4.10.4. General Principles¶
Treat backreferences as untrusted input. Anything captured from the URL (
$1,$2, …) or fromRewriteCond(%1,%2, …) is user-controlled.Prefer relative paths in substitutions. A substitution starting with
/stays on your server. A substitution that could be manipulated intohttp://...becomes a redirect or proxy to an attacker’s host.Use the
[B]flag when passing backreferences into query strings to prevent injection of additional parameters.Least privilege: Don’t use
[P]when[PT]or a simple internal rewrite suffices. Don’t expose more of the filesystem than necessary.