URL Rewrite Techniques for IIS
The IIS URL Rewrite module is a powerful tool for shaping HTTP requests and responses. This article covers a range of techniques I've used with URL Rewrite, from enforcing HTTPS and canonical hostnames to securing cookies with the right modifiers. Each section includes the relevant web.config snippets so you can drop them straight into your own setup.
Canonical Host URLs
Let's start with canonical host URLs.
What is a canonical host url?
It can be incredibly easy to distribute different urls for your website. Lets say that we have www.site.com/post1 and site.com/post1. If you're a search engine, you think that these are different different pages with the same content. When you have the same content, you ranking decreases because the content is not original. As a human looking at the site, we think that the pages are the same content on the same site, so we don't see the duplication.
So a canonical url is basically saying to the search engine: hey, www.site.com/post1 and site.com/post1 are the same content and you should access me through this link (which we define to be a single one of the above).
We can set this through metadata on our webpages, but this doesn't guarentee that people will distribute the correct link to your site. To fix this, we can rewrite the url to either append or remove the www.. In this case, because I prefer shorter url's, we will remove the www sub-domain prefix.
Making a canonical host url via URL Rewrite
To fix this, we create an inbound rule (which lives under system.web > rewrite > rules in our web.config):
<rule name="Canonical Hostname" stopProcessing="true">
<match url="(.*)" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="false">
<add input="{HTTP_HOST}" pattern="^www\.([.a-zA-Z0-9]+)$"/>
</conditions>
<action type="Redirect" url="https://{C:1}/{R:1}" redirectType="Temporary" />
</rule>In the first line, we are defining the name of the rule which can be viewed inside of inetmgr (IIS Manager) saying that we want to stop at this rule and return the response to the client. Next up, we give our matching criteria which we have wildcarded to match all requests. Then we have added a condition which takes a server variable ({HTTP_HOST} which tells us the DNS name or IP address that has been used to reach this server eg: site.com or www.site.com), and we've matched it against any request starting with www..
Finally, we define the action that we wish to take if both the match and conditions sections have been satisfied. The action that we wish to take is to redirect the user to a non www. version of the page. In order to do this, we use one of the capture groups (which comes from the conditions section regex) to redirect to (in this case it's {C:1}) and combine it with the original path and query.
And that's it, you should be redirecting people from say www.site.com to site.com.
Ensuring httpOnly cookies with URL Rewrite
Next, let's look at cookies and how we can ensure they are httpOnly via URL Rewrite using outbound rewrite rules with pre-conditions.
What are cookies?
Unfornately, in this sense they are not a snack. They are, usually, small bits of information continually transferred between the browser and the server in order to do things like session tracking & identification, or maintain informantion pertainent to your browsing session. If not secured in the right way, you can expose details of your browsing session unknowingly. First up, we can make the cookies httpOnly.
What are httpOnly cookies?
By adding the modifier to the cookie, we prevent any scripts running from accessing the cookie. The biggest benefit here is protection against Cross-Site Scripting, or XSS. Unless you have very specific requirements for cookies, this flag should always be enabled.
For more information, checkout Scott Helme's incredible post on getting tougher cookies.
How can we ensure our cookies are httpOnly with URL Rewrite
When a server indicates that it wants to set a cookie, it does so by sending the Set-Cookie HTTP header along with the response. There are a few modifiers that this can have to make them more secure in compliant browsers (eg: Chrome, Firefox, Edge, Safari): httpOnly, secure and sameSite=(lax|strict).
This rewrite snippet requires two portions: the rule and a set of precondtions.
<rewrite>
<outboundRules>
<rule name="Ensure httpOnly Cookies" preCondition="Missing httpOnly cookie">
<match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" />
<action type="Rewrite" value="{R:0}; HttpOnly" />
</rule>
<preConditions>
<preCondition name="Missing httpOnly cookie">
<!-- Don't remove the first line! -->
<add input="{RESPONSE_Set_Cookie}" pattern="." />
<add input="{RESPONSE_Set_Cookie}" pattern="; HttpOnly" negate="true" />
</preCondition>
</preconditions>
</outboundRules>
</rewrite>Within our rule, we are defining the name of the rule which can be viewed inside of inetmgr (IIS Manager). Unlike inbound rules, here we want to continue processing rewrite rules because we may want to do additional work to the response. Next, we match the server varible for a Set-Cookie HTTP header (RESPONSE_Set_Cookie) and ensure that it's present for us to continue. For our action, we rewrite the Set-Cookie header to be the original value, with the HttpOnly modifier appended.
Within the precondition, which is matched by name to the preCondition attribute in the rule, we do two things:
- (I think, see below) Make sure that the
Set-Cookieheader has been set (via the server variable{RESPONSE_Set_Cookie}); - Make sure that we do not already have the
HttpOnlymodifier set
For an unknown reason, probably due to a knowledge gap, the first line is required. I have made a guess as to what this could be, but I am unsure. Strange things happen in the dev tools in Chrome if that first line is not there.
It is worth noting that a precondition is not limited to a single rule, it can be re-used. There is probably scope for making this rule smaller, if there is i'll edit the post to reflect the smaller rule.
That's it. If you check your debug tool of choice after implementing this, you should see that all cookies are now sent with the HttpOnly modifier.
Ensuring samesite cookies with URL Rewrite
We can further increase our website's level of protection against Cross-Site Request Forgery and Cross-Site Script Inclusion attacks by appending an additional modifier to the Set-Cookie HTTP header.
What are SameSite cookies?
In a cross-origin request context, which is when you request resources from a different site, any cookies that you have for that site are also sent. We can restrict this through applying the SameSite modifier to the Set-Cookie HTTP header.
The goals of SameSite cookies are to:
- Prevent cross origin timing attacks;
- Prevent cross origin script inclusion;
- Prevent cross site request forgery;
- Increasing privacy protections
When the modification is applied to the Set-Cookie HTTP header, the browser will only send cookies in a first party context (ie: when you are using the website directly).
There are two possible modes for the attribute: Strict & lax. The difference is which HTTP verbs the security policy should be applied to. In lax mode, cookies are not sent when POSTing to a third party site. In strict mode, cookies are not sent when GETting or POSTing to a third party site. Strict mode is the default mode if the mode is obmitted.
Ensuring our cookies are marked SameSite with URL Rewrite
Using the same outbound rule pattern, we create a rule under system.web > rewrite > outboundRules in our web.config. This rewrite snippet requires two portions: the rule and a set of preconditions:
<rewrite>
<outboundRules>
<rule name="Ensure samesite Cookies" preCondition="Missing samesite cookie">
<match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" />
<action type="Rewrite" value="{R:0}; SameSite=strict" />
</rule>
<preConditions>
<preCondition name="Missing samesite cookie">
<!-- Don't remove the first line here, it does do stuff! -->
<add input="{RESPONSE_Set_Cookie}" pattern="." />
<add input="{RESPONSE_Set_Cookie}" pattern="; SameSite=strict" negate="true" />
</preCondition>
</preconditions>
</outboundRules>
</rewrite>Within our rule, we are defining the name of the rule which can be viewed inside of inetmgr (IIS Manager). Next, we match the server varible for a Set-Cookie HTTP header (RESPONSE_Set_Cookie) and ensure that it's present for us to continue. For our action, we rewrite the Set-Cookie header to be the original value, with the SameSite modifier appended with the mode set to strict as detailed above. Alternatively, you can use SameSite=lax for the lax mode of operation.
Within the precondition, which is matched by name to the preCondition attribute in the rule, we do two things:
- (I think, see below) Make sure that the
Set-Cookieheader has been set (via the server variable{RESPONSE_Set_Cookie}); - Make sure that we do not already have the
SameSitemodifier set
As with the httpOnly rule, the first line is required within the pre-condition or funky things happen.
That's it. If you check your debug tool of choice after implementing this, you should see that all cookies are now sent with the SameSite modifier.
Ensuring secure cookies with URL Rewrite
We can also ensure that cookies are only sent over secure connections by appending the secure modifier to the Set-Cookie HTTP header.
What are secure cookies?
As the name suggests, by appending secure to the Set-Cookie HTTP header, we instruct a browser to only send the cookie when the connection to the web server is secure. This helps protect against any information leakage or eves-dropping.
For more information, checkout Scott Helme's incredible post on getting tougher cookies.
Ensuring our cookies are marked secure with URL Rewrite
Following the same outbound rule pattern, we create a rule under system.web > rewrite > outboundRules in our web.config. This rewrite snippet requires two portions, the rule and a set of preconditions:
<rewrite>
<outboundRules>
<rule name="Ensure secure Cookies" preCondition="Missing secure cookie">
<match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" />
<action type="Rewrite" value="{R:0}; secure" />
</rule>
<preConditions>
<preCondition name="Missing secure cookie">
<!-- Don't remove the first line here, it does do stuff! -->
<add input="{RESPONSE_Set_Cookie}" pattern="." />
<add input="{RESPONSE_Set_Cookie}" pattern="; secure" negate="true" />
</preCondition>
</preconditions>
</outboundRules>
</rewrite>Within our rule, we are defining the name of the rule which can be viewed inside of inetmgr (IIS Manager). Next, we match the server varible for a Set-Cookie HTTP header (RESPONSE_Set_Cookie) and ensure that it's present for us to continue. For our action, we rewrite the Set-Cookie header to be the original value, with the secure modifier appended.
Within the precondition, which is matched by name to the preCondition attribute in the rule, we do two things:
- (I think, see below) Make sure that the
Set-Cookieheader has been set (via the server variable{RESPONSE_Set_Cookie}); - Make sure that we do not already have the
securemodifier set
As with the httpOnly rule, the first line is required within the pre-condition or funky things happen.
That's it. If you check your debug tool of choice after implementing this, you should see that all cookies are now sent with the secure modifier.
Removing trailing slashes with URL Rewrite
For the same SEO reasons that canonical host URLs matter, trailing slashes also need to be handled. Search engines will see site.com/post1 as different content to site.com/post1/ so we need to correct this to get a bit of SEO benefit.
In this case, I will be redirecting any URL that comes with a slash at the end to one without.
Adding or removing trailing slashes via URL Rewrite
We create another inbound rule (which lives under system.web > rewrite > rules in our web.config):
<rule name="Remove trailing slash" stopProcessing="true">
<match url="(.*)/$" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="false">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Redirect" url="{R:1}" redirectType="Temporary" />
</rule>In the first line, we are defining the name of the rule which can be viewed inside of inetmgr (IIS Manager) saying that we want to stop at this rule and return the response to the client. Next up, we give our matching criteria which we have wildcarded to match all requests that end with a /. Then we have added a condition set which is definted to match all criteria inside. We use the server variable {REQUEST_FILENAME} to determine whether or not the path requested is a physical file or directory on disk. The last attribute in each condition is a negation, which is equivilent of saying don't match a physical directory or file (we don't want to screw the routing for those!).
Finally, we define the action that we wish to take if both the match and all the conditions in the conditions section have been satisfied. The action that we wish to take is to redirect the user to the page without the slash, so we use the request capture group {R:1}.
And that's it, you should be redirecting people from say site.com/post1/ to site.com/post1.
Up and running with URL Rewrite - going from HTTP to HTTPS
Now let's look at the foundational URL Rewrite concepts and how to set up the most common rule: redirecting HTTP to HTTPS.
What is URL Rewrite?
URL Rewrite is a module extensions for IIS which allows administrators to create powerful inbound and outbound rules that alter a HTTP request or response. Scenarios where you might consider using the URL Rewrite module include:
- Rewrite URLs based on the values of server variables or HTTP headers
- Provide SEO benefits such as canonical host names
- Setup redirects from old posts to new
URL rewrite can be install from the Web Platform Installer for IIS 7.5 or above.
URL Rewrite and Web.Config
Inside of the web.config you can define a <rewrite> section beneath the <system.web> section:
<system.web>
<rewrite>
<!-- Your rules here -->
</rewrite>
</system.web>Within the <rewrite> section you can define either a <rules> section or an <outbound> section. The former is for inbound request rewriting and the latter is for response rewriting.
Note: if you don't have the URL Rewrite module installed, you'll get a lovely yellow screen of death when you start your application.
Redirecting HTTP to HTTPS
The are some obvious benefits to serving pages over a secure connection (HTTPS), which include:
- Encryption: The data exchanged between client and server cannot easily be read
- Authority: Proving that the server you are talking to is who you are expecting to talk to
However, there are a few lesser known benefits including:
- SEO: Search engines are preferring websites that are secure by default
- HTTP2: Major browsers do not support HTTP2 over insecure connections
Assuming you would like all of the above benefit, here is the rule snippet that you will need:
<system.web>
<rewrite>
<rules>
<rule name="Redirect to http" patternSyntax="Wildcard" stopProcessing="true">
<match url="*" negate="false" />
<conditions logicalGrouping="MatchAny" trackAllCaptures="false">
<add input="{HTTPS}" pattern="off" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Temporary" />
</rule>
</rules>
</rewrite>
</system.web>So what's going on in the snippet above?
In the first line, we are defining the name of the rule which can be viewed inside of inetmgr (IIS Manager); saying which pattern style we are going to use (either regex or wildcard) and saying that we want to stop at this rule and return the response to the client. Next up, we give our matching criteria which we have wildcarded to match all requests. Then we have added a condition which takes a server variable ({HTTPS} which indicates whether or not this request has been delivered over a secure connection), and made sure we only track insecure requests.
With conditions inside of a rule, you can match a single entry in the condition set or all the conditions within the set. Use either MatchAny or MatchAll depending on your needs.
Finally, we define the action that we wish to take if both the match and conditions sections have been satisfied. The action type can be Redirect or Rewrite depending on your needs. We wish to redirect a user to a secure connection, so we define the type as Redirect and the url equal to a secure connection to the host requested with the original path & query appended. The final attribute is the redirect type which can be one of the following:
- Permenant: A HTTP 301 Response
- Found: A HTTP 302 Response
- SeeOther: A HTTP 303 Response
- Temporary: A HTTP 307 Response
That's it! Hopefully this article gives you everything that you need to get HTTP to HTTPS redirects working.