4. RewriteRule

Quickly, bring me a beaker of wine, that I may wet
my 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:

RewriteRule flag summary

Flag

Purpose

B

Escape backreferences before applying them

C

Chain this rule to the next rule

CO

Set a cookie

DPI

Discard path info

E

Set an environment variable

END

Stop processing and don’t re-run in per-directory context

F

Return 403 Forbidden

G

Return 410 Gone

H

Force a content handler

L

Last rule — stop processing this ruleset

N

Re-run from the top (next round)

NC

Case-insensitive match

NE

Don’t escape special characters in the output

NS

Skip if this is an internal sub-request

P

Proxy the request

PT

Pass through to the next URL mapping handler

QSA

Append the original query string

QSD

Discard the original query string

QSL

Use the last ? as the query string delimiter

R

External redirect (optionally with status code)

S

Skip the next N rules

T

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 from RewriteCond (%1, %2, …) is user-controlled.

  • Prefer relative paths in substitutions. A substitution starting with / stays on your server. A substitution that could be manipulated into http://... 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.