Max
Max

Reputation: 683

Best way to do ip-filter policy with X-Forwarded-For

The APIM ip-filter policy doesn't support changing to check X-Forwarded-For instead of request IP. I've tried using the check-header policy, but I get the X-Forwarded-For header with port suffixed <ip>:<port>. I haven't found a way to make the check-header check more advanced to take care of this (removing the port part).

What I've come up with is this, but I don't like it, very bulky and a big if clause.

Can anyone see how this can be solved in a simpler and neater way?

<inbound>
    <base />
    <set-variable name="allowedIPs" value="192.192.192.192" />
    <!-- Determine the IP address to check -->
    <set-variable name="clientIP" value="@(context.Request.Headers.GetValueOrDefault("X-Forwarded-For", context.Request.IpAddress))" />
    <!-- The regular expression ^([\d\.]+) captures only the numeric part of the IP address (i.e., the part before any colon : that would indicate the port). -->
    <set-variable name="cleanClientIP" value="@(System.Text.RegularExpressions.Regex.Match((string)context.Variables["clientIP"], @"^([\d\.]+)").Value)" />
    <!-- Function to check if the IP is allowed -->
    <choose>
        <when condition="@(context.Variables.GetValueOrDefault<string>("allowedIPs").Split(',').Contains(context.Variables.GetValueOrDefault<string>("cleanClientIP")))">
            <!-- REQUEST IS OK, ADD FURTHER LOGIC HERE -->
        </when>
        <otherwise>
            <return-response>
                <set-status code="403" reason="Forbidden" />
                <set-header name="Content-Type" exists-action="override">
                    <value>application/json</value>
                </set-header>
                <set-body>{
"statusCode": 403,
"message": "Not authorized"
}</set-body>
            </return-response>
        </otherwise>
    </choose>
</inbound>

What I mainly want to achieve is:

  1. Early exit (no big if clause)
  2. IPs in a list instead of in a comma separated string

This you get by using check-header or ip-filter. See example below:

<check-header name="header name" failed-check-httpcode="code" failed-check-error-message="message" ignore-case="true | false">
    <value>Value1</value>
    <value>Value2</value>
</check-header>

Upvotes: 0

Views: 56

Answers (1)

Suresh Chikkam
Suresh Chikkam

Reputation: 3448

Simplify the policy by avoiding regular expressions altogether. By splitting the header value using Split(':'), the policy becomes much cleaner and easier to read.

Simplified APIM policy:`

<inbound>
    <base />
    
    <!-- Define the list of allowed IPs -->
    <set-variable name="allowedIPs" value="192.192.192.192,203.0.113.0" />
    
    <!-- Extract the client IP, removing the port if present -->
    <set-variable name="cleanClientIP" value="@(context.Request.Headers.GetValueOrDefault("X-Forwarded-For", context.Request.IpAddress).Split(':')[0])" />
    
    <!-- Check if the cleaned client IP is in the list of allowed IPs -->
    <choose>
        <when condition="@(context.Variables["allowedIPs"].Split(',').Contains(context.Variables["cleanClientIP"]))">
            <!-- REQUEST IS OK, ADD FURTHER LOGIC HERE -->
        </when>
        <otherwise>
            <!-- Deny the request if the IP is not allowed -->
            <return-response>
                <set-status code="403" reason="Forbidden" />
                <set-header name="Content-Type" exists-action="override">
                    <value>application/json</value>
                </set-header>
                <set-body>
                    {
                        "statusCode": 403,
                        "message": "Not authorized"
                    }
                </set-body>
            </return-response>
        </otherwise>
    </choose>
</inbound>
  • Request Header(without port): X-Forwarded-For: 192.168.1.1

Policy extracts 192.168.1.1 from the header and matches it with the allowedIPs list.

Response:

{
  "message": "Request successfully processed"
}

Valid IP with Port:

  • Policy extracts 192.168.1.1 by splitting the header value at the colon (:) It matches 192.168.1.1 with the allowedIPs list.

Response:

{
  "message": "Request successfully processed"
}

Aswell as it works with Multiple IPs in X-Forwarded-For Header also.

Modified:

<inbound>
    <base />
    <!-- Rewrite X-Forwarded-For to remove any port information -->
    <set-header 
        name="X-Forwarded-For" 
        exists-action="override" 
        value="@(context.Request.Headers.GetValueOrDefault('X-Forwarded-For', context.Request.IpAddress).Split(':')[0])" />
    
    <!-- Early exit if the IP is not in the allowed list -->
    <check-header 
        name="X-Forwarded-For" 
        failed-check-httpcode="403" 
        failed-check-error-message="Not authorized" 
        ignore-case="false">
        <value>192.192.192.192</value>
        <value>192.192.192.193</value>
        <!-- Additional allowed IPs can be added here -->
    </check-header>

    <!-- Further processing can go here -->
</inbound>
  • <set-header> rewrites the X-Forwarded-For by splitting its value on the colon (:) and taking the first part, which is the actual IP address without the port.

Now, the policy checks if the cleaned IP value matches any of the allowed IPs listed as separate <value> entries. If there's no match, it automatically returns a 403 response with your custom error message.

Upvotes: 1

Related Questions