SOP - Same Origin Policy

What is Origin?

Two URLs have the same origin if they have the same

  • protocol
  • port
  • host

Example:http://example.com/index.html
Same origin:

SOP
  • A web application using APIs can only request HTTP resources from the same origin the application was loaded from, unless the response from the other origin includes the right CORS headers.
  • Most browsers comply SOP for security reason. For example, when you logged into Facebook and visit a malicious website in another Chrome tab, without SOP, Javascript on that website can do anything with your Facebook account, because it can access cookies saved in browser.
  • Postman is a dev tool => it doesn't care about SOP
  • SOP is not imposed over the <script> tag, allowing scripts to be loaded across different domains
  • The same origin policy is not enforced for all requests, some can fetch resource from any domain:
    • <script>
    • <img>, <video>, <audio>
    • <link>
    • <frame>, <iframe>

CORS - Cross-Origin Resource Sharing

  • Is a mechanism to bypass SOP
  • A web application executes a cross-origin HTTP request when it requests a resource that has a different origin that its own origin. Example: frontend app served from https://example.com call API to backend app served from https://api.example.com
  • Using CORS, we can define which origin can request the server's origin. In the response, some headers will be added to guide the browser:
    • Access-Control-Allow-Origin: indicate browser what client-domains are allowed to access server's resource
    • Access-Control-Allow-Credentials: This header is only required to be present in the response if your server supports authentication via cookies.
    • Access-Control-Allow-Headers: list of request header values the server is willing to support
    • Access-Control-Expose-Headers: list of headers that will be present in the actual response
    • Access-Control-Allow-Methods: list of HTTP request type verbs (eg. GET, POST) which the server is willing to support.

Implement CORS in Spring Boot

  • Server side: a simple REST API /hello response a String "Hello, world"
package com.example.democors;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {
    @GetMapping("/hello")
    public String greeting() {
        return "Hello, world";
    }
}
  • Client side: ajax call the server side api
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>CORS</title>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
  <script>
    $(document).ready(function() {
      $.ajax({
        url: "http://localhost:81/hello"
      }).then(function(data, status, jqxhr) {
        $('.greeting-id').append(data);
        console.log(jqxhr);
      });
    });
  </script>
</head>
<body>
  <div>
    <p class="greeting-id">Response: </p>
  </div>
</body>
</html>

Run backend and frontend in different port:

# backend
./gradlew bootRun -Dserver.port=81 
# frontend
./gradlew bootRun -Dserver.port=82

Notice the error in browser console:
Screenshot-2019-07-10-at-2.54.26-PM

Now add the CORS anotation to the controller:

package com.example.democors;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {
    @CrossOrigin(origins = "http://localhost:82")
    @GetMapping("/hello")
    public String greeting() {
        return "Hello, world";
    }
}

and try run again the backend

Screenshot-2019-07-10-at-2.56.16-PM
Other attributes can be configured to @CrossOrigin: methods, allowedHeaders, exposedHeaders, allowCredentials or maxAge.

Another way to implement CORS in Spring Boot is via a global config:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
 
    @Override
    public void addCorsMappings(CorsRegistry registry) {
       registry.addMapping("/hello").allowedOrigins("http://localhost:82");
    }
}
JSONP - Another way to bypass SOP

JSONP based on the fact that <script> is not restricted by SOP.

We created a function in frontend to add a script tag to the frontend page, this tag contains the src = http://localhost:81/helloCallback with a callback parameter.
So when it received the response, response data will be passed to myCallback function

<script>
    function callJsonp(){
      var target = document.createElement('script');
      target.src = "http://localhost:81/helloCallback?callback=myCallback";
      document.body.appendChild(target);
    }

    function myCallback(data) {
      $('.greeting-id-callback').append(data);
    }
</script>
<body>
  <div>
    <p class="greeting-id">Response: </p>
    <p class="greeting-id-callback">Response from JSONP: </p>
    <script>
      window.onload=function(){
        callJsonp();
      }
    </script>
  </div>
</body>

In backend, we need to response with content type: application/javascript:

@GetMapping("/helloCallback")
    public void greetingCallback(HttpServletRequest request, HttpServletResponse response, @RequestParam(value = "callback") String callback)
                    throws IOException {
        PrintWriter out = response.getWriter();
        response.setContentType("application/javascript");
        out.print(callback + "('Hello, callback');");
    }

Result:

Screenshot-2019-07-10-at-4.09.45-PM

Spring 4 provides AbstractJsonpResponseBodyAdvice class for handling JSONP, but AbstractJsonpResponseBodyAdvice was deprecated starting from Spring 5.0.7 and 4.3.18 and in version 5.1 it is completely removed. So we can mimic the AbstractJsonpResponseBodyAdvice like above. However, the recommedation is to use @CrossOrigin to better control the CORS.

That's all about JSONP: it's a callback and script tags.

Source code: https://github.com/quangh33/CORS-demo-spring-boot

P/S:
JSONP stands for JSON with Padding, a misleading name since it really has nothing to do with "padding".