Workshop: HTTP Messages

The effect of request and response headers and the purpose of CORS.

Style: guided

Overview

- Before starting this workshop, check you have the requisite tools and setup.

- By the end of this workshop, you will be able to:

  • Set request and response headers
  • Explain CORS from the perspective of front-end web development.

Exercises

A set of exercises to test your understanding of http request and response headers.

  1. Same-origin policy in the browser

    Browser: start the http server and open a new web browser. Enter this request in the address bar:

    http://localhost:3000?colour=red

    The response is returned in the browser (and logged to the console).

    The response can be accessed because the requesting origin is the same as the resource origin.

    The origin is composed of scheme/domain/port. In this case, http/localhost/3000.

  2. Unsuccessful request

    Browser: start the http client and open it in the browser.

    The same-origin policy (SOP) prevents requests from a web Browser (one origin) accessing the response to a request sent to a different origin.

    Start up the http client and send a request to our local http server using the example: http://localhost:3000/?colour=red

    The request will fail:

    Failed to fetch
  3. Successful request

    Code: uncomment the Access-Control-Allow-Origin header on the http server.

    The wildcard value (*) instructs the browser to allow access to the response from any origin i.e. it allows the JavaScript running on any Browser in the browser to access the response.

    Access-Control-Allow-Origin: *

    Send the example request again.

    This time the response is displayed and the response returns with a status of 200 (success).

    {
                    "name": "red",
                    "hex": "#ff0000"
                  }
  4. CORS: HTTP header-based protocol

    Cross-Origin Resource Sharing (CORS) allows requests that SOP would otherwise prevent.

    Code: comment out the Access-Control-Allow-Origin header on the http server again.

    Send the example request. Once again, the request fails.

    Open up browser development tools. In the console you will see a message like this:

    Access to fetch at 'http://localhost:3000/?colour=red' from origin 'http://127.0.0.1:1234' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

    The CORS error is also recorded in Status field of the network tab.

    Now check the server console.

    You will see that although the browser has blocked access to the response, the server processed the request, and send a valid response.

    A matching requesting origin

    Code: change the value of Access-Control-Allow-Origin so that the origin matches that of the requesting origin (the web browser in the browser).

    You can compare the values of origin by clicking on the request in the network tab (Tip: view the headers in raw mode).

    Request Header

    Origin: http://127.0.0.1:1234

    Response Header

    Access-Control-Allow-Origin: http://127.0.0.1:1234

    The allowed origin now matches the Origin 3 passed in the request (set unless specified otherwise by the browser).

    The request response is once more accessible.

    A non-matching requesting origin

    Code: change the value of Access-Control-Allow-Origin so that either the scheme, domain or port does not match e.g. use https.

    Note that the request is always returned by the http server and is visible in the http server console.

    Access-Control-Allow-Origin: https://127.0.0.1:1234

    NB The value of the port (1234) can vary. Check the value in the client browser address bar. The match must be exact; localhost is not the same as 127.0.0.1 and vice versa.

    It is not possible to enter multiple origins but it is possible to check a URL against an allowed list. If there is a match, the request origin is set as the allowed origin.

    CORS summary

    Cross-Origin Resource Sharing (CORS) is a security mechanism implemented by browsers. The value of Access-Control-Allow-Origin affects whether or not the browser passes the response onto the web browser client.

    It is not a way to safeguard access to the API - the server returns a response no matter the Access-Control-Allow-Origin value.

  5. Clients other than browsers

    Terminal: open a new terminal window.

    Clients other than browsers can access a request resource regardless of the value of Access-Control-Allow-Origin. This can be verified by replicating a request for which the response is unavailable to a web browser from the command line. One way to do this is by using cURL:

    curl -i http://localhost:3000/?colour=red

    The response is logged out to the terminal.

    HTTP/1.1 200 OK
                    Access-Control-Allow-Origin: *
                    Connection: keep-alive
                    Keep-Alive: timeout=5
                    Content-Length: 30
                  
  6. Setting the value of mode in the request header

    Code: set the value of mode to no-cors in the client.

    By default, the value of mode in the client header is cors. This value appears as Sec-Fetch-Mode on the network tab in Chrome DevTools.

    Changing this value to no-cors has no effect on the server response header Access-Control-Allow-Origin.

    no-cors instructs the browser not to send the origin of the request. As a result, the response cannot be accessed. See the section on no-cors in the appendix for reasons why you might want to do this.

    The server response is also different:

    Response status: 0
  7. POST request

    Browser: click on the FETCH POST tab.

    A standard post has no CORS requirements other than that Access-Control-Allow-Origin permits the request.

    NB The POST request will be made even when the response from the server is unavailable!

  8. Preflight request

    Code: uncomment the content type header in the client.

    Where a request can cause side-effects, the browser will send a preflight request to the server to check whether the request will be fulfilled. This request is generated by the browser, not the developer, and has a method of type OPTIONS.

    As stated, a standard POST does not generate a preflight request. If the client request sets certain headers, however, a preflight request may be made.

    headers: {
                    'Content-Type': 'application/json',
                  },

    This client request header will result in a CORS warning. As a consequence, the POST request will not be sent.

    To allow this header, it is necessary to explicitly accept the header type on the server:

    Code: uncomment the Access-Control-Allow-Headers setting on the server.

    res.setHeader('Access-Control-Allow-Headers', 'Content-Type')

    The preflight OPTIONS request now passes and the POST request is also sent.

    Status code

    When a new colour is added, the status is 201 Created.

    If the colour exists, the status is 409 Conflict.

  9. Form submission

    Browser: click on the FORM POST tab.

    Form submissions are unaffected by CORS. No preflight request will be sent.

    To prevent Cross-Site Request Forgery (CSRF), the server must be defended against malicious actions. This topic is not covered here.

    CORS only triggers a preflight requests for requests not regarded as "simple". This term is no longer used but is useful nonetheless.

    See the section on Simple requests in the appendix for more detail.

Summary

In this workshop we set up a simple http server, an instance of Node.js's http.Server.

From a web browser, we made a number of GET and POST requests against this server using the fetch API. We also posted data from an HTML form without JavaScript.

We experimented with headers on both the server and client in order to demonstrate that CORS is enabled by configuring the server but is used by the browser to control whether or not a web browser (and the JavaScript running on it) has access to the response.

Browser policy mechanism

There is a lot of confusion around CORS and LLMs are no more reliable than many other sources. This summary from Claude, however, is correct:

CORS is better described as a "browser policy mechanism" rather than a "security feature". It's designed to enforce the Same-Origin Policy (SOP) in browsers, but doesn't provide true security against malicious actors.

What CORS is about

  1. Browser policy enforcement
  2. Controlling which web applications can interact with your API directly from browsers
  3. Developer experience (allowing legitimate cross-origin requests when needed)

What CORS isn't about

  1. Preventing server-to-server attacks
  2. Protecting against stolen credentials
  3. Real security against determined attackers
Claude

How to…

Further reading

Appendix

  1. no-cors

    There are valid reasons for setting mode to no-cors in a request header. This is a verbatim answer from Claude made in response to my question asking why I might want to set mode to no-cors:

    The 'no-cors' mode in a client request header is used in a few specific scenarios:

    1. Cross-Origin Resource Sharing (CORS) Restrictions: If you're making a request to a resource that is hosted on a different origin (different domain, subdomain, port, or protocol) than your web application, the browser may block the request due to CORS restrictions. Setting the mode to 'no-cors' allows you to bypass these restrictions and make the request, but it also means you won't be able to access the response data in your JavaScript code due to the same-origin policy.
    2. Accessing Resources from Content Delivery Networks (CDNs): When making requests to resources (e.g., fonts, images, scripts) hosted on a CDN, you may need to use 'no-cors' mode to bypass CORS checks and load the resources correctly in your web application.
    3. Interoperability with Legacy Systems: Sometimes, you may need to interact with older or legacy systems that don't properly implement CORS headers. Setting 'no-cors' mode can help you work around these issues and make the necessary requests.
    Claude

    Back to referring text

  2. Simple requests

    This is a helpful summary from ChatGTP:

    Preflight requests are only sent for cross-origin requests that don’t meet the criteria for "simple" requests, such as when:

    1. The request method is not GET, POST, or HEAD.
    2. The request includes custom headers beyond a few permitted ones (Content-Type, Accept, etc.).
    3. The Content-Type is not one of the following allowed types:
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/plain
    ChatGPT

    Back to referring text

  3. The difference between origin and referer

    The Origin is the origin of the request.

    The Referer contains the absolute or partial address from which a resource has been requested. It can therefore contain the path, querystring in addition to the origin.

    Back to referring text