Clickjacking Protection Headers in Java: Complete Implementation Guide

Clickjacking (UI redress attack) is a malicious technique that tricks users into clicking something different from what they perceive. Proper HTTP headers are crucial for protecting against these attacks.

Understanding Clickjacking Protection Headers

Primary Headers for Clickjacking Protection

  1. X-Frame-Options: Legacy but widely supported
  2. Content-Security-Policy (frame-ancestors): Modern standard
  3. Clear-Site-Data: Additional cleanup protection

Spring Security Implementation

Example 1: Basic Spring Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Clickjacking protection
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions
.deny() // or .sameOrigin()
)
.contentSecurityPolicy(csp -> csp
.policyDirectives("frame-ancestors 'none'")
)
.xssProtection(xss -> xss
.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)
)
)
.authorizeHttpRequests(authz -> authz
.anyRequest().permitAll()
);
return http.build();
}
}

Example 2: Advanced Header Configuration

@Configuration
@EnableWebSecurity
public class AdvancedSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
// Frame options for clickjacking protection
.frameOptions(frameOptions -> frameOptions
.sameOrigin() // Allow same origin framing
)
// Content Security Policy
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://trusted.cdn.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"frame-ancestors 'self' https://trusted.example.com; " +
"form-action 'self'")
)
// X-Content-Type-Options
.contentTypeOptions(contentType -> {})
// Strict Transport Security
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.preload(true)
.maxAgeInSeconds(31536000) // 1 year
)
// Referrer Policy
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
// Permissions Policy
.permissionsPolicy(permissions -> permissions
.policy("camera=(), microphone=(), location=(), payment=()")
)
);
return http.build();
}
}

Manual Servlet Filter Implementation

Example 3: Custom Security Headers Filter

@Component
public class SecurityHeadersFilter implements Filter {
private static final String CSP_POLICY = 
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://trusted.cdn.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-ancestors 'none'; " +
"form-action 'self'; " +
"base-uri 'self';";
@Override
public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain) throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Clickjacking protection headers
httpResponse.setHeader("X-Frame-Options", "DENY");
httpResponse.setHeader("Content-Security-Policy", CSP_POLICY);
// Additional security headers
httpResponse.setHeader("X-Content-Type-Options", "nosniff");
httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
httpResponse.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
httpResponse.setHeader("Permissions-Policy", 
"camera=(), microphone=(), location=(), payment=()");
// Cache control for sensitive pages
if (isSensitivePage((HttpServletRequest) request)) {
httpResponse.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
httpResponse.setHeader("Pragma", "no-cache");
httpResponse.setDateHeader("Expires", 0);
}
chain.doFilter(request, response);
}
private boolean isSensitivePage(HttpServletRequest request) {
String path = request.getRequestURI();
return path.contains("/admin") || 
path.contains("/account") || 
path.contains("/payment");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Initialization if needed
}
@Override
public void destroy() {
// Cleanup if needed
}
}

Example 4: Dynamic CSP Header Filter

@Component
public class DynamicCspFilter implements Filter {
private final CspPolicyProvider cspPolicyProvider;
public DynamicCspFilter(CspPolicyProvider cspPolicyProvider) {
this.cspPolicyProvider = cspPolicyProvider;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Generate dynamic CSP based on request
String cspPolicy = cspPolicyProvider.generatePolicy(httpRequest);
String nonce = generateNonce();
// Replace nonce placeholder in CSP
cspPolicy = cspPolicy.replace("'nonce-{nonce}'", "'nonce-" + nonce + "'");
httpResponse.setHeader("Content-Security-Policy", cspPolicy);
httpResponse.setHeader("X-Frame-Options", "DENY");
// Store nonce in request for use in templates
httpRequest.setAttribute("cspNonce", nonce);
chain.doFilter(request, response);
}
private String generateNonce() {
byte[] nonceBytes = new byte[16];
new SecureRandom().nextBytes(nonceBytes);
return Base64.getEncoder().encodeToString(nonceBytes);
}
}
@Service
class CspPolicyProvider {
public String generatePolicy(HttpServletRequest request) {
String path = request.getRequestURI();
// Different policies for different sections
if (path.startsWith("/admin")) {
return adminPolicy();
} else if (path.startsWith("/public")) {
return publicPolicy();
} else if (path.startsWith("/api")) {
return apiPolicy();
} else {
return defaultPolicy();
}
}
private String defaultPolicy() {
return "default-src 'self'; " +
"script-src 'self' 'nonce-{nonce}'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-ancestors 'none'; " +
"form-action 'self'; " +
"base-uri 'self';";
}
private String adminPolicy() {
return "default-src 'self'; " +
"script-src 'self' 'nonce-{nonce}'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"frame-ancestors 'none'; " +
"form-action 'self'; " +
"base-uri 'self';";
}
private String publicPolicy() {
return "default-src 'self' https://cdn.example.com; " +
"script-src 'self' https://cdn.example.com 'nonce-{nonce}'; " +
"style-src 'self' https://cdn.example.com 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self' https://fonts.googleapis.com; " +
"connect-src 'self'; " +
"frame-ancestors 'self' https://embed.example.com; " +
"form-action 'self'; " +
"base-uri 'self';";
}
private String apiPolicy() {
return "default-src 'none'; " +
"frame-ancestors 'none';";
}
}

Spring Boot Configuration Properties

Example 5: Application Properties Configuration

# application.yml
server:
servlet:
session:
cookie:
secure: true
http-only: true
same-site: strict
spring:
security:
headers:
content-security-policy: "default-src 'self'; frame-ancestors 'none';"
frame-options: DENY
content-type: true
xss-protection: true
hsts:
include-subdomains: true
preload: true
max-age: 31536000
# Custom security properties
app:
security:
csp:
enabled: true
report-only: false
policy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'"
allowed-frame-ancestors:
- "https://trusted.example.com"
- "https://embed.partner.com"

Controller-Level Configuration

Example 6: Fine-Grained Header Control

@RestController
public class SecureController {
@GetMapping("/public/content")
public ResponseEntity<String> publicContent() {
return ResponseEntity.ok()
.header("X-Frame-Options", "SAMEORIGIN")
.header("Content-Security-Policy", "frame-ancestors 'self'")
.body("Public content that can be framed by same origin");
}
@GetMapping("/sensitive/account")
public ResponseEntity<String> sensitiveContent() {
return ResponseEntity.ok()
.header("X-Frame-Options", "DENY")
.header("Content-Security-Policy", "frame-ancestors 'none'")
.header("Cache-Control", "no-store, no-cache, must-revalidate")
.body("Sensitive account information");
}
@GetMapping("/embed/widget")
public ResponseEntity<String> embeddableWidget() {
String allowedOrigins = "https://trusted.example.com https://partner.com";
return ResponseEntity.ok()
.header("X-Frame-Options", "ALLOW-FROM https://trusted.example.com")
.header("Content-Security-Policy", "frame-ancestors " + allowedOrigins)
.body("Embeddable widget");
}
@GetMapping("/api/data")
public ResponseEntity<Map<String, Object>> apiData() {
Map<String, Object> data = Map.of("message", "API response");
return ResponseEntity.ok()
.header("X-Frame-Options", "DENY")
.header("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'")
.body(data);
}
}

Example 7: Annotation-Based Header Configuration

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SecurityHeaders {
FrameOptions frameOptions() default FrameOptions.DENY;
String[] frameAncestors() default {};
boolean noCache() default false;
enum FrameOptions {
DENY, SAMEORIGIN, ALLOW_FROM
}
}
@Aspect
@Component
public class SecurityHeadersAspect {
@Around("@annotation(securityHeaders)")
public Object applySecurityHeaders(ProceedingJoinPoint joinPoint, 
SecurityHeaders securityHeaders) throws Throwable {
if (joinPoint.getTarget() instanceof HttpServletResponse) {
HttpServletResponse response = (HttpServletResponse) joinPoint.getArgs()[1];
applyHeaders(response, securityHeaders);
}
return joinPoint.proceed();
}
private void applyHeaders(HttpServletResponse response, SecurityHeaders securityHeaders) {
// Apply frame options
switch (securityHeaders.frameOptions()) {
case DENY:
response.setHeader("X-Frame-Options", "DENY");
break;
case SAMEORIGIN:
response.setHeader("X-Frame-Options", "SAMEORIGIN");
break;
case ALLOW_FROM:
if (securityHeaders.frameAncestors().length > 0) {
response.setHeader("X-Frame-Options", "ALLOW-FROM " + 
securityHeaders.frameAncestors()[0]);
}
break;
}
// Apply CSP frame-ancestors
if (securityHeaders.frameAncestors().length > 0) {
String frameAncestors = String.join(" ", securityHeaders.frameAncestors());
response.setHeader("Content-Security-Policy", 
"frame-ancestors " + frameAncestors);
} else {
response.setHeader("Content-Security-Policy", "frame-ancestors 'none'");
}
// Apply cache control
if (securityHeaders.noCache()) {
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.setHeader("Pragma", "no-cache");
}
}
}
@RestController
public class AnnotatedController {
@GetMapping("/admin/dashboard")
@SecurityHeaders(frameOptions = SecurityHeaders.FrameOptions.DENY, noCache = true)
public String adminDashboard() {
return "Admin dashboard";
}
@GetMapping("/public/widget")
@SecurityHeaders(
frameOptions = SecurityHeaders.FrameOptions.ALLOW_FROM,
frameAncestors = {"https://trusted.example.com", "https://partner.com"}
)
public String publicWidget() {
return "Public widget";
}
}

Testing Clickjacking Protection

Example 8: Security Headers Testing

@SpringBootTest
@AutoConfigureTestDatabase
class ClickjackingProtectionTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void testClickjackingHeadersOnPublicEndpoint() {
ResponseEntity<String> response = restTemplate.getForEntity("/public/content", String.class);
HttpHeaders headers = response.getHeaders();
// Test X-Frame-Options
assertThat(headers.getFirst("X-Frame-Options")).isEqualTo("DENY");
// Test CSP frame-ancestors
assertThat(headers.getFirst("Content-Security-Policy"))
.contains("frame-ancestors");
// Test additional security headers
assertThat(headers.getFirst("X-Content-Type-Options")).isEqualTo("nosniff");
assertThat(headers.getFirst("X-XSS-Protection")).isNotNull();
}
@Test
void testSensitiveEndpointHasStrictHeaders() {
ResponseEntity<String> response = restTemplate.getForEntity("/admin/dashboard", String.class);
HttpHeaders headers = response.getHeaders();
// Test strict frame options
assertThat(headers.getFirst("X-Frame-Options")).isEqualTo("DENY");
assertThat(headers.getFirst("Content-Security-Policy"))
.contains("frame-ancestors 'none'");
// Test no-cache headers
assertThat(headers.getFirst("Cache-Control"))
.contains("no-store");
assertThat(headers.getFirst("Pragma")).isEqualTo("no-cache");
}
@Test
void testEmbeddableWidgetAllowsFraming() {
ResponseEntity<String> response = restTemplate.getForEntity("/embed/widget", String.class);
HttpHeaders headers = response.getHeaders();
// Test specific frame ancestors
assertThat(headers.getFirst("Content-Security-Policy"))
.contains("frame-ancestors https://trusted.example.com https://partner.com");
}
}
@Component
class SecurityHeadersValidator {
public ValidationResult validateHeaders(HttpHeaders headers) {
ValidationResult result = new ValidationResult();
// Check required security headers
checkFrameOptions(headers, result);
checkCsp(headers, result);
checkAdditionalHeaders(headers, result);
return result;
}
private void checkFrameOptions(HttpHeaders headers, ValidationResult result) {
String frameOptions = headers.getFirst("X-Frame-Options");
if (frameOptions == null) {
result.addIssue("MISSING", "X-Frame-Options header is missing");
} else if (!frameOptions.equals("DENY") && !frameOptions.equals("SAMEORIGIN")) {
result.addIssue("WEAK", "X-Frame-Options value might be weak: " + frameOptions);
}
}
private void checkCsp(HttpHeaders headers, ValidationResult result) {
String csp = headers.getFirst("Content-Security-Policy");
if (csp == null) {
result.addIssue("MISSING", "Content-Security-Policy header is missing");
} else if (!csp.contains("frame-ancestors")) {
result.addIssue("WEAK", "CSP missing frame-ancestors directive");
} else if (csp.contains("frame-ancestors *")) {
result.addIssue("INSECURE", "CSP allows framing from any origin");
}
}
private void checkAdditionalHeaders(HttpHeaders headers, ValidationResult result) {
if (headers.getFirst("X-Content-Type-Options") == null) {
result.addIssue("MISSING", "X-Content-Type-Options header is missing");
}
if (headers.getFirst("X-XSS-Protection") == null) {
result.addIssue("MISSING", "X-XSS-Protection header is missing");
}
}
@Data
public static class ValidationResult {
private List<SecurityIssue> issues = new ArrayList<>();
private boolean passed = true;
public void addIssue(String severity, String description) {
issues.add(new SecurityIssue(severity, description));
passed = false;
}
}
@Data
public static class SecurityIssue {
private final String severity;
private final String description;
}
}

Best Practices and Recommendations

Security Configuration Best Practices

@Configuration
public class SecurityBestPractices {
@Bean
public SecurityFilterChain bestPracticeFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
// Always set both X-Frame-Options and CSP for maximum compatibility
.frameOptions(frameOptions -> frameOptions.deny())
.contentSecurityPolicy(csp -> csp
.policyDirectives(buildCspPolicy())
)
// Prevent MIME type sniffing
.contentTypeOptions(ContentTypeOptionsConfig::disable)
// Enable XSS protection
.xssProtection(xss -> xss.block(true))
// HSTS for HTTPS sites
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.preload(true)
.maxAgeInSeconds(31536000)
)
// Referrer policy
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
// Feature policy (now Permissions Policy)
.permissionsPolicy(permissions -> permissions
.policy("camera=(), microphone=(), geolocation=(), payment=()")
)
)
// Additional security configurations
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/public/**")
)
.sessionManagement(session -> session
.sessionFixation().migrateSession()
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
);
return http.build();
}
private String buildCspPolicy() {
return String.join("; ",
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://trusted.cdn.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self'",
"frame-ancestors 'none'",
"form-action 'self'",
"base-uri 'self'",
"object-src 'none'"
);
}
}

Header Implementation Matrix

HeaderPurposeRecommended ValueBrowser Support
X-Frame-OptionsBasic clickjacking protectionDENY or SAMEORIGINWidespread
Content-Security-PolicyModern clickjacking protectionframe-ancestors 'none'Modern browsers
X-Content-Type-OptionsPrevent MIME sniffingnosniffWidespread
X-XSS-ProtectionBasic XSS protection1; mode=blockLegacy browsers

Conclusion

Clickjacking protection is essential for web application security. Key recommendations:

  1. Use Both Headers: Implement both X-Frame-Options and Content-Security-Policy for maximum compatibility
  2. Default to DENY: Unless specifically required, prevent all framing
  3. Be Specific with Allowances: When allowing framing, specify exact origins
  4. Test Thoroughly: Regularly test headers with automated tools
  5. Monitor Reports: Use CSP report-uri to detect potential issues

Remember that security headers are just one layer of defense. Always implement proper authentication, authorization, and input validation alongside header-based protections.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper