cancel
Showing results for 
Search instead for 
Did you mean: 
Login & Join the DevCentral Connects Group to watch the Recorded LiveStream (May 12) on Basic iControl Security - show notes included.
Rory_Hewitt_F5_
Altocumulus
Altocumulus

Problem this snippet solves:

Pretty self-explanatory - we had to implement CORS (Cross-Origin-Resource-Sharing) where we had multiple domains, all of which had to be able to make AJAX calls to API's in our 'api.example.com' subdomain. Additionally, we had some partners who also need to be able to call our API's. In some cases, we had to pass cookies in the request.

In the past, various developers had created backend Java code to return the CORS response headers, but almost invariably they did an incomplete job - either returning an invalid value or not returning all the required headers or writing the code such that it wasn't portable across applications. Therefore, I decided to write some 'common' CORS handling code, which would have the benefit of doing 'proper' origin checking and would also immediately return the OPTIONS preflight response directly from F5, thus improving performance.

After much hacking around, here is what I came up with.

We used a class to define multiple top-level domains as 'allowed' origins - this would contain both your domains and also those of any partners whom you want to allow to make CORS requests to your site.

If you just have multiple subdomains on a single domain (e.g. www.example.com, api.example.com, code.example.com), you could simply use [HTTP::header Origin] ends_with ".example.com" - it's a little simpler.

I'm always returning the Access-Control-Allow-Credentials: true response header for 'valid' non-OPTIONS (e.g. GET/POST) CORS requests, even if it's not required (i.e. even if the withCredentials flag was not set in the request) - unfortunately, there is no way to know whether it is needed simply by looking at the request, so it's the only way to ensure client errors don't occur.

I'm passing the value of the Access-Control-Request-Method request header in the Access-Control-Allow-Methods response header (e.g. a single value of 'GET' or 'POST' or whatever) - in most implementations, you'll see people returning somethign like a string like Access-Control-Allow-Methods: GET, POST, PUT, but there's really no significant benefit to doing this - much simpler to only return what is passed. In either case, it will be cached by the browser because of the Access-Control-Max-Age response header.

Note that because you will be returning a specific Access-Control-Allow-Origin value, rather than '*', you should also return the Vary: Origin response header. This may have issues with browser caching or if you use a CDN like Akamai or Cloudflare - you should consult any CDN product documentation. There are multiple good sources for explaining the Vary header - Google is your friend.

If anyone has any comments, please add them, good or bad! I would love to know if someone finds this snippet useful...

Code :

# Domains that are allowed to make cross-domain calls to example.com
class allowed_origins {
    ".example.com"
    ".example2.com"
    ".goodpartner.com"
}

when HTTP_REQUEST {
    unset -nocomplain cors_origin
    if { [class match [HTTP::header Origin] ends_with allowed_origins] } {
        if { ( [HTTP::method] equals "OPTIONS" ) and ( [HTTP::header exists "Access-Control-Request-Method"] ) } {
            # CORS preflight request - return response immediately
            HTTP::respond 200 "Access-Control-Allow-Origin" [HTTP::header "Origin"] \
                              "Access-Control-Allow-Methods" [HTTP::header "Access-Control-Request-Method"] \
                              "Access-Control-Allow-Headers" [HTTP::header "Access-Control-Request-Headers"] \
                              "Access-Control-Max-Age" "86400" \
                              "Vary" "Origin"
        } else {
            # CORS GET/POST requests - set cors_origin variable
            set cors_origin [HTTP::header "Origin"]
        }
    }
}

when HTTP_RESPONSE {
    # CORS GET/POST response - check cors_origin variable set in request
    if { [info exists cors_origin] } {
        HTTP::header insert "Access-Control-Allow-Origin" $cors_origin
        HTTP::header insert "Access-Control-Allow-Credentials" "true"
        HTTP::header insert "Vary" "Origin"
    }
}

Tested this on version:

11.0
Comments
gis-att_206935
Nimbostratus
Nimbostratus

Looks great. Reading some of the KB articles, it seems to indicate that I need the ASM module to use CORS headers. Do you know if this is true? (I have a pending ticket, but, unfortunately, this question is actively holding me up.

 

EDIT: ASM module is not required. This can be done via iRule. F5 support pointed me specifically to this page.

 

jrojaso89_19793
Nimbostratus
Nimbostratus

Hello,

 

I have solved a issue applying this irule.

 

Thank you

 

Cyril_M
Nimbostratus
Nimbostratus

Very useful code, and you can add that it solves an issue with the cookie persistence : when a browser sends a pre-flight request, by definition in RFC2616 it doesn't send any cookie with the OPTIONS method. So when the bigip receives the OPTIONS request, without cookie, it considers it as a new request, round robin the pool member, and in the http_reply the bigip includes a set_cookie ... The browser receives the set_cookie, thus drops the one it already had ... And voilà you lost your session 😞

 

So with this iRule, the bigip doesn't send the pre-flight request to the pool member, and doesn't send a set_cookie back => your browser will keep the session cookie

 

Maybe F5 should consider a feature to not reply with a set_cookie to an OPTIONS request ?

 

Rory_Hewitt_F5_
Altocumulus
Altocumulus

@Cyril, your comment about new request pooling messing with cookies hadn't occurred to me - very good point!

 

Cyril_M
Nimbostratus
Nimbostratus

I didn't challenge the F5 support yet to know if this whole "disable set-cookie when replying to an OPTIONS request" is something that can be done out of the box (and what firmware ?) or if using an iRule is mandatory ...

 

DrLemongelo
Nimbostratus
Nimbostratus

After hours of struggling with syntax errors, support helped identify a correction I'll pass along.

 

1) I wasn't able to define the class inside of the iRule and had to create a Data Group object (type: string) that contained the allowed origins as String with a blank value.

 

If the though of using a GUI brings a tear to your eye, here's the CLI:

 

ltm data-group internal DG-CORS-ALLOWED-ORIGINS { records { .authorizeddomain.com { } localhost:3456 { } } type string }

2) The syntax in OP's iRule required the addition of more brackets.

 

This lines gives me an error: /Common/IRULE-CORS:10: error: [parse error: PARSE syntax 228 {syntax error in expression " class match [HTTP::header Origin] ends_with allowed_origins...": variable references require preceding $}][{ class match [HTTP::header Origin] ends_with allowed_origins }]

 

if { class match [HTTP::header Origin] ends_with DG-CORS-ALLOWED-ORIGINS }

 

Enclosed class in brackets and received no syntax errors:

 

if { [class match [HTTP::header Origin] ends_with DG-CORS-ALLOWED-ORIGINS] }

 

This was tested in LTM 11.6.0

 

Rory_Hewitt_F5_
Altocumulus
Altocumulus

Code snippet updated to include additional square brackets in the class match statement, as per the comment from @DrLemongelo.

 

Kevin_Dyer
Nimbostratus
Nimbostratus

I have not noticed any further comment about responses to set-cookie, so I thought I would respond. Section 7.1.5 Cross-Origin Request with Preflight of the latest CORS specification (https://www.w3.org/TR/cors/) states the client shall make the OPTIONS request with the "block cookie" flag set. For newer HTTP clients this means any response to an OPTIONS request with Origin header must ignore all set-cookie headers. But as others on this thread have noted, not all HTTP clients are up to date on handling set-cookie for a preflight response. Therefore, to err on the side of caution, I too strongly recommend handling the CORS preflight requests as close to the edge of the network as possible.

 

To piggyback on security settings I've also taken to add the header Content-Security-Policy: frame-ancestors 'self' '$cors_origin'; when a Origin header was found otherwise respond with the header Content-Security-Policy: frame-ancestors 'self';

 

Rory_Hewitt_F5_
Altocumulus
Altocumulus

@Kevin_Dyer note that the reference you cite for latest CORS specification is actually significantly out-of-date.

 

The responsibility for CORS was taken over by WHATWG, as part of the fetch spec (https://fetch.spec.whatwg.org/). The examples aren't great, but fetch now does allow (in theory) some enhancements, such as allowing an asterisk as the value for the Access-Control-Allow-Headers, Access-Control-Allow-Methods and Access-Control-Expose-Headers CORS response headers. I don't know whether all browsers have implemented support for these special values yet, however.

 

Jeremy_D_
Nimbostratus
Nimbostratus

Hello, I would like to Apply this Irules, but i have an error when I create this.

 

01070151:3: Rule [/Common/IR_CROSS_DOMAIN_XXXXX.XXX] error: /Common/IR_CROSS_DOMAIN_XXXXX.XXX:2: error: [command is not valid in the current scope][class allowed_origins { ".XXXXX.XXX" }]

 

Anyone have a idea ?

 

Version : BIG-IP 11.5.3 Build 1.0.167 Hotfix HF1

 

Cyril_M
Nimbostratus
Nimbostratus

You can put this class definition AFTER the "when HTTP_REQUEST {" and it should be fine

 

krejzi_banan_19
Nimbostratus
Nimbostratus

I've got the same problem like Jeremy Desca any idea?

 

Cyril_M
Nimbostratus
Nimbostratus

Well, didn't my previous comment solve the issue ?

 

Luan_de_Mattos_
Nimbostratus
Nimbostratus

Mr. Jeremy Desca, you need to make the modification that Mr. DrLemongelo has to write in his comment, you need to abandon the class use, and after that your irule will be not have more syntax issues, trust me I have tested.

 

Kind regards.

 

Tom_L
Nimbostratus
Nimbostratus

I stopped trying to use class, and tried to utilize the Data group object with the recommended class match brackets, but keep getting the error below when I try to Update/Save the iRule.

 

01070151:3: Rule [/Common/CORS-Response-Header-iRule] error: /Common/CORS-Response-Header-iRule:1: error: [undefined procedure: ltm][ltm data-group internal DG-CORS-ALLOWED-ORIGINS {

records {

.XXXXXXXX.com { }

localhost:443 { }

}

type string

}]

 

Version BIG-IP 14.1.2.3 Build 0.0.5 Release 3

Tom_L
Nimbostratus
Nimbostratus

Disregard my last post. This is now resolved. This was an issue on my end with defining the Data Group.

 

Nicol4s
Nimbostratus
Nimbostratus

Hi Tom,

 

Can you post your datagroup and Irule as an example please ?

 

Thank you very much.

Frederic_d_HUA1
Nimbostratus
Nimbostratus

You should create a data group called "allowed_origins" type string and add the list of your domains to it. You can then make a call to this data group from an iRule.

 

madel
Nimbostratus
Nimbostratus

Kindly What do you mean about create data group ?

Version history
Last update:
‎02-Nov-2015 16:01
Updated by:
Contributors