SOP (Same-Origin Policy) jest mechanizmem bezpieczeństwa przeglądarek internetowych
polegający na odizolowaniu od siebie zasobów różnego pochodzenia. Każdy zasób webowy ma źródło, z którego pochodzi tzw. origin. Jeden unikalny origin definiują jego 3 główne składowe tj. protokół
, domena
i port
.
Przykładowo http://api.example.com/app.js
http://api.example.com:8090/app.js
http://example.com/app.js
https://example.com/app.js
są różnymi originami mimo, że mogą wskazywać na ten sam zasób. Z drugiej stronyhttp://example.com/example1.html
http://example.com/example2.html
są różnymi zasobami jednak pochodzą z tego samego origin'a.
W praktyce pod jednym origin zwykle mamy aplikację webową
składającą się z wielu różnych zasobów aby jej funkcjonowanie było możliwe.
Przeglądarka implementując SOP w podstawowym zakresie zabezpiecza naszą aplikację
przed różnego rodzaju potencjalnym zagrożeniem ze strony bezpieczeństwa gdzie inna aplikacja np. stworzona przez atakującego mogłaby z wykorzystaniem AJAX wywoływać metody w imieniu użytkownika naszej aplikacji np. kupić produkt w sklepie uprzednio zmieniając adres wysyłki na atakującego czyli tzw. atak CSRF (Cross-site request forgery). Warto wspomnieć, że nie należy polegać jedynie na tym mechaniźmie i stosować różnego rodzaju zabezpiczenia po stronie serwera tj. csrf token, walidacja origin, nagłówki access control i inne zabezpieczenia.
W celu przetestowania mechanizmu SOP wystarczy wejść na dowolną stronę np. google.com
otworzyć narzędzia developerskie w zakładce Console
i wysłać żądanie Ajaxowe na inny origin:
var req = new XMLHttpRequest();
req.open('GET', 'http://localhost:8081', false);
req.send();
Access to XMLHttpRequest at 'http://localhost:8081/' from origin 'https://www.google.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.Jak widać przeglądarka zablokowała możliwość odczytania / dostępu do odpowiedzi z uwagi na inny origin, z którego pobieramy zasób niż ten, w kontekście którego aktualnie się znajdujemy czyli google.com
. Warto zaznaczyć, że przeglądarka jedynie zablokowała dostęp do odpowiedzi natomiast samo żądanie zostało wysłane poprawnie. Wynika to z faktu, że przeglądarka dzieli żądania na „proste” czyli np. wykonywane w kontekście img
, iframe
, style
, form
i „złożone” np. typu PUT
, DELETE
(więcej o różnicy tutaj).
Wiemy już dlaczego nie możemy polegać tylko na mechaniźmie SOP. Atakujący w łatwy sposób mógłby spreparować tzw. „proste” żądanie przygotowując np. odpowiedni
formularz, który automatycznie by został wysłany do serwera zmieniając stan systemu w niekontrolowany sposób.
Z uwagi na to, że SOP jest dość restrykcyjny ponieważ pozwala na dostęp do zasobów tylko z tego samego origin, CORS (Cross-Origin Resource Sharing) ma za zadanie zluzować tą sztywną politykę i umożliwić za pomocą odpowiedniej konfiguracji na bezproblemową komunikację pomiędzy różnymi origin'ami.
Przykładowo aby odblokować dostęp do odpowiedzi, która wcześniej była przyblokowana przez przeglądarkę możemy po stronie serwera w odpowiedzi ustawić nagłówek w taki sposób:
Access-Control-Allow-Origin: *
W ten sposób zapewnimy dostęp dla danego zasobu wszystkim aplikacjom (origins), które będą chciały uzyskać do niego dostęp. Zwykle powyższy nagłówek ustawiany jest w taki sposób dla zasobów statycznych dostępnych publicznie np. biblioteki javascriptowe. Jest to typowe dla sieci CDN:
$ curl -I https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
HTTP/2 200
content-type: application/javascript; charset=utf-8
access-control-allow-origin: *
server: cloudflare
...
Przykładowa konfiguracja CORS w Springu
Może się zdarzyć tak, że aplikacja kliencka po stronie przeglądarki (np. Angular) będzie musiała komunikować się z api serwera w innym origin niż jest serwowana np. aplikacja napisana w Angular jest dostępna pod adresem https://example.com
i komunikuje się z backendem poprzez https://api.example.com
. Wiemy już, że są to dwa różne origin'y w związku z tym aby komunikacja przebiegła pomyślnie musimy w taki sposób skonfigurować backend aby zezwolić na taką komunikację.
Rozpoczniemy od zdefiniowania beana odpowiedzialnego za dostarczenie ww. konfiguracji:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@ConditionalOnProperty(prefix = "cors", name = "allowedOrigins")
public class CorsConfiguration {
@Bean
public CorsConfigurationSource corsConfigurationSource(
@Value("${cors.allowedOrigins}") List allowedOrigins
) {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
org.springframework.web.cors.CorsConfiguration corsConfiguration = new org.springframework.web.cors.CorsConfiguration();
corsConfiguration.setAllowedOrigins(allowedOrigins);
corsConfiguration.addAllowedHeader("*");
corsConfiguration.setAllowedMethods(List.of(
HttpMethod.GET.name(),
HttpMethod.HEAD.name(),
HttpMethod.POST.name(),
HttpMethod.PUT.name(),
HttpMethod.DELETE.name()
));
corsConfiguration.setMaxAge(3600L);
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
}
Następnie stworzymy adnotację pomocniczą, która zaimportuje nam ww. konfigurację.
Zakładam, że konfiguracja będzie w jednej z bibliotek wspólnych w związku z tym pozwoli to nam ją włączać w zależności od potrzeb dla modułów, które wystawiają api restowe.
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({CorsConfiguration.class})
public @interface EnableCorsConfiguration {
}
Po czym wykorzystamy ww. konfigurację:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@Configuration
@EnableResourceServer
@EnableCorsConfiguration
public class ExampleSecurityConfiguration extends ResourceServerConfigurerAdapter {
@Value("${oauth.resourceId}")
private String resourceId;
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers().antMatchers("/**")
.anyRequest().authenticated().and().cors();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(resourceId);
}
}
Na koniec w pliku application.yaml
zdefiniujemy na jakie origin'y zezwalamy:
cors:
allowedOrigins: https://example.com
Od teraz aplikacja kliencka bez problemu powinna komunikować się z backendem.
Warto wspomnieć, że jeżeli przed naszymi aplikacjami backendowymi w Java stoi jakiś reverse proxy np. Nginx tam również byłaby możliwa konfiguracja poprzez manipulację nagłówkami.
Na koniec warto zastanowić się czy architektura, w której API mamy na oddzielnej domenie faktycznie ma sens. Przeniesienie go na główną domenę może zredukować liczbę wykonywanych żądań nawet o połowę (z uwagi na Preflight Requests) co na pewno odciąży nasz serwer.