Why this matters
- Misconfigured CORS blocks legitimate frontend clients or accidentally allows any origin to call your API.
- CSRF attacks trick authenticated users into performing unwanted actions — dangerous for cookie-based sessions.
- A pre-deployment checklist catches the security gaps that code review misses.
CORS configuration
When the BookStore React frontend (https://app.bookstore.example.com) calls the API (https://api.bookstore.example.com), the browser enforces CORS:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.bookstore.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Authorization", "Content-Type", "X-Correlation-Id")
.exposedHeaders("X-Correlation-Id")
.maxAge(3600);
}
}
Or via Spring Security:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors(cors -> cors.configurationSource(request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.bookstore.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
return config;
}));
// ... authorization rules
return http.build();
}
CORS rules for BookStore
allowedOrigins— Explicit origin list. Never use*with credentials.allowedMethods— HTTP verbs the frontend may use.allowedHeaders— Request headers the frontend may send.exposedHeaders— Response headers the frontend JavaScript can read.maxAge— How long the browser caches preflight results.
CSRF protection
CSRF exploits cookie-based sessions: a malicious site submits a form to your API using the victim's session cookie.
For stateless JWT APIs: disable CSRF — tokens in headers are not sent automatically by browsers.
http.csrf(csrf -> csrf.disable());
For cookie-based sessions: enable CSRF with token validation:
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
The frontend reads the XSRF-TOKEN cookie and sends it as X-XSRF-TOKEN header.
Security headers
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.deny())
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true))
);
| Header | Purpose |
|---|---|
| Content-Security-Policy | Restrict resource loading sources |
| X-Frame-Options: DENY | Prevent clickjacking |
| Strict-Transport-Security | Force HTTPS for one year |
| X-Content-Type-Options: nosniff | Prevent MIME type sniffing |
Content-Security-Policy
PurposeRestrict resource loading sourcesX-Frame-Options: DENY
PurposePrevent clickjackingStrict-Transport-Security
PurposeForce HTTPS for one yearX-Content-Type-Options: nosniff
PurposePrevent MIME type sniffing
Production security checklist
Before deploying BookStore
- Secrets in environment variables — No passwords in
application.ymlcommitted to git. - HTTPS everywhere — Terminate TLS at the load balancer or ingress; redirect HTTP to HTTPS.
- Actuator endpoints secured —
/actuator/**requires authentication or is disabled. - Swagger UI disabled — Or protected behind admin auth in production.
- Default credentials removed — No
user/passwordfrom Boot's default security. - SQL injection prevented — Use parameterized queries (JPA handles this); never concatenate SQL.
- Input validation enabled —
@Validon all request DTOs. - Dependencies updated — Run
./mvnw versions:display-dependency-updatesregularly. - Logging sanitised — Never log passwords, tokens, or full credit card numbers.
- Rate limiting configured — At the gateway or API level to prevent abuse.
Rate limiting example
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Map<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>();
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String clientIp = request.getRemoteAddr();
int count = requestCounts
.computeIfAbsent(clientIp, k -> new AtomicInteger(0))
.incrementAndGet();
if (count > 100) {
response.setStatus(429);
return;
}
chain.doFilter(request, response);
}
}
Production rate limiting belongs at the API gateway (Kong, AWS WAF) — in-app filters are a starting point.
Error response hygiene
Ensure production error responses never leak internals:
@ExceptionHandler(Exception.class)
public ProblemDetail handleUnexpected(Exception ex) {
log.error("Unhandled exception", ex); // full trace in logs
return ProblemDetail.forStatus(500); // generic message to client
}
Stack traces, SQL errors, and file paths in HTTP responses are information disclosure vulnerabilities.
Quick recall
Everything you need if you only revisit this box.
- CORS configures which browser origins can call BookStore API endpoints.
- Never use
allowedOrigins("*")when credentials (cookies) are involved. - Disable CSRF only for stateless token-based APIs; keep it for cookie sessions.
- Set security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options.
- Production checklist: env-var secrets, HTTPS, secured actuator, no default creds.
- Never expose stack traces or internal details in production error responses.
Test yourself
Answer these before moving on — recall is what makes it stick.