Forum Discussion
Jeremy_Church_3
Cirrus
Hello Tony,
Thoughts on the XFF header:- Contents of X-Forwarded-For are not guaranteed to be valid or accurate IP addresses.
- Multiple headers are possible in a single request.
- If a single XFF header contains multiple addresses, it will likely be comma-space delimited e.g.
though not guaranteed.1.1.1.1, 2.2.2.2
- It is possible to have a combination of multiple XFF headers with some containing multiple addresses.
- The X-Forwarded-For header can be inserted by anyone with any address they choose. It is important to understand filtering by XFF does not provide security.
If possible, it would be preferable if a custom header colud be added similar to what CDNs do, e.g.
True-Client-IP
, to reduce the chance of anomalous values from occuring.
Parsing IPs from X-Forwarded-For
When using the method:
[getfield [HTTP::header values X-Forwarded-For] " " 1]
which is similar to:
[lindex [HTTP::header values X-Forwarded-For] 0]
you may end up with something like:
1.1.1.1,
which is not a valid IP causing a TCL error.
What I have done in the past
I'll be honest, I'm not 100% clear on your 2nd objective, but here is a possible rule based on my interpretation. This may not be the most efficient rule, but it should help to reduce the number of TCL error you may encounter and maybe point you in the right direction.
when HTTP_REQUEST {
if {[class match [HTTP::uri] starts_with DG-ALLOWED-URI-LIST]} {
this is a secure URI
check the real source IP
set MATCH [class match [IP::client_addr] equals DG-ALLOWED-IP-XFF]
cycle the X-Forwarded-For headers
foreach HDR [HTTP::header values X-Forwarded-For] {
if {$MATCH} { break }
format the X-Forwarded-For header into a valid TCL list
foreach IP [string trim [regsub -all {[,; ]+} $HDR " "]] {
if {[catch {set MATCH [class match $IP equals DG-ALLOWED-IP-XFF]}] != 0} {
log local0.err "[virtual name]: Client [IP::client_addr] \
had an invalid X-Forwarded-For address - \x22${IP}\x22"
set MATCH 0
}
if {$MATCH} { break }
}
}
if {!$MATCH} { HTTP::respond 403 Connection close }
unset MATCH
}
}
Notes
- This rule checks your URI once. The example/objective you provided seems to be performing more than one match so it may need to be modified.
- The outside/first
loop cycles each XFF header individually. Usually there is only 1, but it is possible for multiple headers to exist.foreach
- The inside/second
loop usesforeach
+regsub
to strip any seperator characters resulting in a valid TCL list.string trim
- When matching the address with
, theclass match
command is used to prevent a TCL error and a log message created for later review.catch
- Instead of
, the rule is responding with a 403 Forbidden. Thereject
command works too, but my preference is a 403 response which is effectively the HTTP version of reject.reject
- The logic in this rule can also match the real source IP and won't cycle X-Forwarded-For values if it matches. This may or may not be what you need. If not, the rule would need a
in its place.set MATCH 0
Jeremy_Church_3
Mar 25, 2017Cirrus
Tony,
There are a few options. This rule is not the most efficient way to accomplish your request, but it should be simple to understand, maintain and modify if needed.
I'll break the logic up into 3 parts:- Determine if the request is for a protected URL.
- Check if XFF IP is allowed.
- Allow, Redirect or reject depending on results.
when HTTP_REQUEST {
1. Check for secure URL
variable to track if the site is secure
set SEC 0
if {[class match [HTTP::uri] starts_with DG-ALLOWED-URI-LIST]} {
set SEC 1
} elseif { [string match -nocase {*/uri1/uri2/uri3/adminpage*} [HTTP::uri]] } {
set SEC 1
set REDIR "/secret[HTTP::uri]"
}
2. Check if IP/XFF is allowed
if { $SEC } {
this is a secure URL
check the real source IP
set MATCH [class match [IP::client_addr] equals DG-ALLOWED-IP-XFF]
cycle the X-Forwarded-For headers
foreach HDR [HTTP::header values X-Forwarded-For] {
if {$MATCH} { break }
format the X-Forwarded-For header into a valid TCL list
foreach IP [string trim [regsub -all {[,; ]+} $HDR " "]] {
if {[catch {set MATCH [class match $IP equals DG-ALLOWED-IP-XFF]}] != 0} {
log local0.err "[virtual name]: Client [IP::client_addr] \
had an invalid X-Forwarded-For address - \x22${IP}\x22"
set MATCH 0
}
if {$MATCH} { break }
}
}
3. Allow, redirect or reject
check for match and perform redirect if needed
if {$MATCH} {
if {[info exists REDIR]} {
HTTP::respond 302 Location $REDIR
unset REDIR
}
} else {
HTTP::respond 403 Connection close
}
unset MATCH
}
unset SEC
}
Notes
- To match the URI you want to redirect, I used
, but the same can be accomplished withstring match -nocase
Both have the same result.if { [string tolower [HTTP::uri]] contains "/uri1/uri2/uri3/adminpage" }
- This rule only redirects the secure URL if the client has an approved IP. Another option is to redirect anything that matches
regardless of source IP or what's in the XFF header. From the user's perspective, the result would be the same and the rule would be a little shorter and probably a little faster./uri1/uri2/uri3/adminpage
- Instead of using a whole URL in the redirect, the rule performs a relative redirect pre-pending
onto the original URI. When the browser redirects, it will use the same scheme and domain./secure
- If a redirect is needed, it's critical that the URI you redirect to matches something in the DG-ALLOWED-URI-LIST data-group. If not, you will create a redirect loop.
- Instead of using
, this rule usesHTTP::redirect
I prefer this method because it clearly shows it's a 302 redirect and makes it simple to change to a 301 if desired. It also allows adding additional headers to the response.HTTP::respond 302 Location ...
is the same asHTTP::redirect /secure[HTTP::uri]
HTTP::respond 302 Location /secure[HTTP::uri]
If this is a rule you plan to use in a production environment, it is important you test it and understand it.
Let me know how it works.