# Secure URL Redirects using Apache, ModRewrite and ModSecurity If you have anchor tags with href attributes external to your own website, you have no log of when those links are clicked. Example: ```html Click to go to "external.site.example.com" ``` It has become very common for people to use the JavaScript onclick event and ajax to report back to the server when a click has happened, but this is no good if the user doesn't have JavaScript enabled, or if the HTML is on an external site such as an RSS aggregator. For those cases, it has become common to change the href to link back to a script on your own site which performs a HTTP redirect. Example: ```html Click to go to "external.site.example.com" ``` If you just allow any old "url" parameter to your redirect script, this is what is called an "Open Redirect." Open redirects have been abused heavily by scammers and spammers and should not exist. One option is to maintain a whitelist of URLs that are allowed to be redirected to. This is cumbersome to maintain. Another option is to add an additional parameter which authenticates the URL. For example, you could take a hash of the URL, along with a private Salt, and then provide that as a parameter too. For my examples I will use a simple short salt, "ABCDEFGHIJKLM". Here is an example of how I would generate and use such a URL: Generate the authentication token: ```console mike@haven:~$ echo "ABCDEFGHIJKLM_http://external.site.example.com/"|md5sum a54de5ffcb50cead775cac0b254af460 - mike@haven:~$ ``` Build the URL: ```html Click to go to "external.site.example.com" ``` The redirect.cgi is only doing two things. It checks that the MD5 of "ABCDEFGHIJKLM_http://external.site.example.com/" matches a54de5ffcb50cead775cac0b254af460, and if so it redirects. We don't even need to write a script to do that, it can all be done inside the Apache configuration or a htaccess file. Here follows the mod_rewrite configuration: ```apache RewriteEngine On RewriteMap unescape int:unescape RewriteCond %{QUERY_STRING} ^(?:.*\&)?url=([^\&]*%3[fF][^\&]*) RewriteRule ^/bounce$ ${unescape:%1} [R=301,L,NE] RewriteCond %{QUERY_STRING} ^(?:.*\&)?url=([^\&]+) RewriteRule ^/bounce$ ${unescape:%1}? [R=301,L,NE] ``` On its own, the above would create an open redirector. The next step is to add some mod_security configuration to check the auth parameter: ```apache ## Enable ModSecurity and allow HTTP request parsing SecRuleEngine On SecRequestBodyAccess On ## Allow redirects if there is a valid auth parameter matching the url parameter SecRule REQUEST_URI ^/+bounce(\?.*)?$ "chain,phase:1,nolog,allow" SecRule ARGS:auth ^[a-f0-9]{32}$ "chain,setvar:tx.auth=%{MATCHED_VAR}" SecRule ARGS:url ^(?i)https?://.+$ "chain,setvar:tx.urlnsalt=**ABCDEFGHIJKLM**_%{MATCHED_VAR}" SecRule TX:urlnsalt "@streq %{TX.auth}" "t:md5,t:hexEncode" ## Block all other requests for /bounce with a 403 FORBIDDEN error SecRule REQUEST_URI ^/+bounce(\?.*)?$ "phase:1,log,deny,status:403" ``` It's pretty amazing what you can do with mod_security. You might still consider it a pain to convert: http://external.site.example.com/ To: http://my.site.example.com/bounce?url=http://external.site.example.com/&auth=a54de5ffcb50cead775cac0b254af460 But for dynamically generated websites especially, it can be done completely transparently. This website its self is generated using server side XSLT, with a self built framework utilising Perls XML::LibXML and XML::LibXSLT modules. So with a little Perl, XPath and XML trickery, I've been able to update the framework to convert all external anchor tags to use my redirector dynamically. Here's the code: ```perl ## $xml is the XML::LibXML::Document ## $stylesheet is the XML::LibXSLT::Stylesheet ## Apply the stylesheet to the XML to generate the final output my $doc = $stylesheet->transform( $xml, ); ## If the final output is text/html, fixup the anchor tags... if( $stylesheet->media_type eq 'text/html' ){ ## Iterate over each anchor tag foreach my $node ( $doc->findnodes('html/body//a') ){ ## If the href is pointing to an external url my $href = $node->getAttribute('href') || ''; if( $href =~ /^https?:\/\/([-a-z0-9\.]+)/i && lc($1) ne lc($ENV{HTTP_HOST}) ){ ## Overwrite the href attribute $node->setAttribute( 'href', sprintf( '/bounce?auth=%s&url=%s', Digest::MD5::md5_hex( "ABCDEFGHIJKLM_$href" ), URI::Escape::uri_escape( $href ), ) ); } } } ``` Now if I use: ```html Click me ``` in a blog post, my framework automatically converts it to the redirect version, and if someone then clicks the link when reading my RSS feed, even though it was an external link and they weren't viewing the blog post from my website, I still know the link was clicked.