Application Security¶
OWASP Top 10 (2021)¶
A01: Broken Access Control¶
Prevention¶
// BAD: Direct object reference without authorization
@GetMapping("/api/orders/{orderId}")
public Order getOrder(@PathVariable Long orderId) {
return orderRepository.findById(orderId); // Anyone can access any order!
}
// GOOD: Verify ownership before returning data
@GetMapping("/api/orders/{orderId}")
public Order getOrder(@PathVariable Long orderId, Principal principal) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new NotFoundException("Order not found"));
if (!order.getUserId().equals(principal.getName())) {
throw new ForbiddenException("Access denied");
}
return order;
}
// BETTER: Use authorization at repository level
@GetMapping("/api/orders/{orderId}")
public Order getOrder(@PathVariable Long orderId, Principal principal) {
return orderRepository.findByIdAndUserId(orderId, principal.getName())
.orElseThrow(() -> new NotFoundException("Order not found"));
}
A02: Cryptographic Failures¶
Password Hashing¶
// BAD: Plain text or weak hash
String hashedPassword = DigestUtils.md5Hex(password); // NEVER DO THIS
// GOOD: Use bcrypt with appropriate cost factor
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12); // cost factor
String hashedPassword = encoder.encode(rawPassword);
// Verify password
boolean matches = encoder.matches(rawPassword, hashedPassword);
Data Encryption¶
// AES-256-GCM encryption
public class EncryptionService {
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
public byte[] encrypt(byte[] plaintext, SecretKey key) throws Exception {
byte[] iv = new byte[GCM_IV_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] ciphertext = cipher.doFinal(plaintext);
// Prepend IV to ciphertext
byte[] result = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length);
return result;
}
}
A03: Injection¶
SQL Injection Prevention¶
// BAD: String concatenation
String query = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
// GOOD: Parameterized queries
String query = "SELECT * FROM users WHERE username = ?";
PreparedStatement pstmt = connection.prepareStatement(query);
pstmt.setString(1, username);
ResultSet rs = pstmt.executeQuery();
// GOOD: JPA Named Parameters
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);
// GOOD: Criteria API
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
query.where(cb.equal(root.get("username"), username));
Command Injection Prevention¶
// BAD: Direct command execution
Runtime.getRuntime().exec("ping " + userInput);
// GOOD: Use ProcessBuilder with array (no shell interpretation)
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", validatedHost);
pb.redirectErrorStream(true);
Process process = pb.start();
// BETTER: Avoid shell commands entirely, use libraries
InetAddress address = InetAddress.getByName(validatedHost);
boolean reachable = address.isReachable(5000);
A04: Insecure Design¶
Secure Design Patterns¶
// Rate limiting
@RateLimiter(name = "loginAttempts", fallbackMethod = "loginRateLimited")
public AuthResponse login(LoginRequest request) {
// Login logic
}
// Account lockout
public class LoginService {
private static final int MAX_ATTEMPTS = 5;
private static final Duration LOCKOUT_DURATION = Duration.ofMinutes(15);
public AuthResponse login(LoginRequest request) {
User user = userRepository.findByEmail(request.getEmail());
if (user.isLocked() && user.getLockoutExpiry().isAfter(Instant.now())) {
throw new AccountLockedException("Account locked. Try again later.");
}
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
user.incrementFailedAttempts();
if (user.getFailedAttempts() >= MAX_ATTEMPTS) {
user.lock(LOCKOUT_DURATION);
}
userRepository.save(user);
throw new BadCredentialsException("Invalid credentials");
}
user.resetFailedAttempts();
userRepository.save(user);
return generateToken(user);
}
}
A05: Security Misconfiguration¶
Security Headers Configuration¶
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'"))
.frameOptions(frame -> frame.deny())
.xssProtection(xss -> xss.block(true))
.contentTypeOptions(Customizer.withDefaults())
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000))
);
return http.build();
}
}
Error Handling¶
// BAD: Exposing stack traces
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
return ResponseEntity.status(500).body(e.getMessage()); // Leaks info!
}
// GOOD: Generic error messages
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("Internal error", e); // Log details internally
return ResponseEntity.status(500)
.body(new ErrorResponse("An unexpected error occurred",
UUID.randomUUID().toString())); // Correlation ID
}
A06: Vulnerable Components¶
Dependency Scanning¶
<!-- Maven: OWASP Dependency Check -->
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>8.4.0</version>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
</configuration>
</plugin>
# Run dependency check
mvn dependency-check:check
# Gradle
./gradlew dependencyCheckAnalyze
# NPM
npm audit
npm audit fix
# Snyk
snyk test
snyk monitor
Cross-Site Scripting (XSS)¶
XSS Prevention¶
// Server-side: Use templating engine with auto-escaping
// Thymeleaf automatically escapes by default
<p th:text="${userInput}">User input here</p> // Safe
<p th:utext="${userInput}">DANGER</p> // Unescaped - avoid!
// Manual encoding
import org.owasp.encoder.Encode;
String safe = Encode.forHtml(userInput);
String safeJs = Encode.forJavaScript(userInput);
String safeUrl = Encode.forUriComponent(userInput);
// Client-side: Use textContent, not innerHTML
// BAD
element.innerHTML = userInput;
// GOOD
element.textContent = userInput;
// For HTML, use DOMPurify
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);
Content Security Policy¶
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
Cross-Site Request Forgery (CSRF)¶
CSRF Prevention¶
// Spring Security CSRF (enabled by default)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
}
// For APIs using JWT (stateless), CSRF can be disabled
http.csrf(csrf -> csrf.disable()); // Only if using token auth
SameSite Cookies¶
// Set SameSite attribute
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setSameSite("Strict"); // or "Lax"
serializer.setUseSecureCookie(true);
serializer.setUseHttpOnlyCookie(true);
return serializer;
}
Server-Side Request Forgery (SSRF)¶
SSRF Prevention¶
public class UrlValidator {
private static final Set<String> BLOCKED_HOSTS = Set.of(
"localhost", "127.0.0.1", "0.0.0.0", "169.254.169.254"
);
private static final Set<String> ALLOWED_SCHEMES = Set.of("http", "https");
public boolean isUrlSafe(String urlString) {
try {
URL url = new URL(urlString);
// Check scheme
if (!ALLOWED_SCHEMES.contains(url.getProtocol())) {
return false;
}
// Check for blocked hosts
String host = url.getHost().toLowerCase();
if (BLOCKED_HOSTS.contains(host)) {
return false;
}
// Resolve and check IP
InetAddress address = InetAddress.getByName(host);
if (address.isLoopbackAddress() ||
address.isLinkLocalAddress() ||
address.isSiteLocalAddress()) {
return false;
}
return true;
} catch (Exception e) {
return false;
}
}
}
Input Validation¶
Validation Example¶
@RestController
public class UserController {
@PostMapping("/api/users")
public ResponseEntity<User> createUser(@Valid @RequestBody CreateUserRequest request) {
// Request already validated by Bean Validation
return ResponseEntity.ok(userService.create(request));
}
}
public class CreateUserRequest {
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
@Size(max = 255)
private String email;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100)
@Pattern(regexp = "^[a-zA-Z\\s-']+$", message = "Invalid characters in name")
private String name;
@NotBlank
@Size(min = 8, max = 128)
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&]).+$",
message = "Password must contain uppercase, lowercase, number, and special char")
private String password;
@Past
private LocalDate birthDate;
}
Security Testing¶
Common Interview Questions¶
- What is OWASP Top 10?
- List of most critical web security risks
- Updated periodically (latest 2021)
-
Covers injection, broken auth, XSS, etc.
-
How to prevent SQL injection?
- Parameterized queries / prepared statements
- Use ORM frameworks properly
-
Input validation and escaping
-
XSS vs CSRF?
- XSS: Inject malicious scripts into pages
- CSRF: Trick user into unwanted actions
-
Different attack vectors, different mitigations
-
How to store passwords securely?
- Never store plain text
- Use bcrypt, Argon2, or scrypt
-
Use unique salt per password
-
What is defense in depth?
- Multiple layers of security controls
- If one fails, others still protect
- Example: WAF + input validation + parameterized queries
- *