A hands-on guide to understanding web security vulnerabilities and their mitigations
This e-commerce demo application demonstrates all OWASP Top 10 2025 vulnerabilities with both vulnerable and secure implementations side-by-side.
# Backend (Spring Boot)
cd backend && ./mvnw spring-boot:run
# Frontend (React + Vite)
cd frontend && npm run devURLs:
- Admin Panel: http://localhost:8080/admin/dashboard
- API Docs: http://localhost:8080/swagger-ui.html
- Customer App: http://localhost:5173
Test Accounts:
| Role | Password | |
|---|---|---|
| Admin | admin@owasp.demo | Admin123! |
| Customer | john@example.com | Customer123! |
| Customer | jane@example.com | Customer123! |
Users can access resources they shouldn't - like viewing another user's orders.
- Login as
john@example.comand note your order ID - Logout and login as
jane@example.com - Access
/api/owasp/vulnerable/orders/{johnsOrderId}- you can see John's order!
// VulnerableController.java
@GetMapping("/orders/{orderId}")
public OrderResponse getOrderInsecure(@PathVariable Long orderId) {
// No ownership check - any user can access any order
return orderService.getOrderByIdVulnerable(orderId);
}// SecureController.java
@GetMapping("/orders/{orderId}")
public OrderResponse getOrderSecure(
@PathVariable Long orderId,
@AuthenticationPrincipal User user) {
// Ownership verification in service layer
return orderService.getOrderById(orderId, user);
}
// OrderService.java
public OrderResponse getOrderById(Long orderId, User user) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order not found"));
// SECURE: Verify ownership
if (!order.getUser().getId().equals(user.getId())) {
auditLogService.logAccessControlViolation(
"Attempted access to order " + orderId, request);
throw new AccessDeniedException("You don't have access to this order");
}
return mapToResponse(order);
}- Always verify resource ownership before access
- Use role-based access control (RBAC)
- Deny by default - require explicit permission
- Log access control violations
Exposing debug endpoints, default credentials, verbose errors, or unnecessary features.
Access /api/owasp/vulnerable/config to see:
- Database connection strings
- Environment variables
- System paths
- JVM version info
// VulnerableController.java
@GetMapping("/config")
public Map<String, Object> getConfigInsecure() {
Map<String, Object> config = new HashMap<>();
config.put("database.url", "jdbc:h2:mem:owaspdb");
config.put("environment_variables", System.getenv()); // Dangerous!
config.put("user.home", System.getProperty("user.home"));
return config;
}# application.yml
server:
error:
include-stacktrace: never
include-message: never
management:
endpoints:
web:
exposure:
include: health,info # Only safe endpoints// SecureController.java
@GetMapping("/config")
public Map<String, Object> getConfigSecure() {
Map<String, Object> safeConfig = new HashMap<>();
safeConfig.put("application.name", "OWASP E-Commerce Demo");
safeConfig.put("api.version", "v1");
// No database URLs, no env vars, no system properties
return safeConfig;
}- Disable debug endpoints in production
- Remove default credentials
- Configure proper error handling
- Use security headers (CSP, X-Frame-Options)
- Disable directory listing
Using components with known vulnerabilities or from untrusted sources.
<!-- pom.xml - Pin specific versions, use reputable sources -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version> <!-- Explicit, latest secure version -->
</dependency>- Audit dependencies regularly:
./mvnw dependency:tree - Use OWASP Dependency-Check plugin
- Subscribe to CVE notifications
- Use lock files (package-lock.json, pom.xml versions)
- Verify checksums of downloaded packages
Exposing sensitive data, weak encryption, storing passwords in plaintext.
Access /api/owasp/vulnerable/users/1 to see:
- Password hash exposed
- Failed login attempts
- Account lock status
- All user roles
// VulnerableController.java
@GetMapping("/users/{userId}")
public Map<String, Object> getUserInsecure(@PathVariable Long userId) {
return userRepository.findById(userId).map(user -> {
Map<String, Object> sensitiveData = new HashMap<>();
sensitiveData.put("email", user.getEmail());
sensitiveData.put("password_hash", user.getPassword()); // NEVER!
sensitiveData.put("failedLoginAttempts", user.getFailedLoginAttempts());
return sensitiveData;
}).orElse(null);
}// SecureController.java
@GetMapping("/users/me")
public UserProfileResponse getUserSecure(@AuthenticationPrincipal User user) {
// DTO controls exactly what's exposed
return UserProfileResponse.builder()
.id(user.getId())
.email(user.getEmail())
.firstName(user.getFirstName())
.lastName(user.getLastName())
// password, roles, failedAttempts are NEVER exposed
.build();
}
// SecurityConfig.java
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // Strong cost factor
}- Use BCrypt/Argon2 for password hashing
- Use DTOs to control data exposure
- Encrypt sensitive data at rest
- Use TLS for data in transit
- Never log sensitive information
User input directly concatenated into SQL, OS commands, or other interpreters.
- Go to
/api/owasp/vulnerable/products/search?name=' OR '1'='1 - All products are returned (SQL injection worked!)
- Try
/api/owasp/vulnerable/products/search?name='; DROP TABLE products; --
// VulnerableController.java
@GetMapping("/products/search")
public List<?> searchProductsInsecure(@RequestParam String name) {
// VULNERABLE: String concatenation in SQL
String sql = "SELECT * FROM Product p WHERE p.name LIKE '%" + name + "%'";
Query query = entityManager.createQuery(sql);
return query.getResultList();
}// ProductRepository.java - Parameterized query
@Query("SELECT p FROM Product p WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', :search, '%'))")
Page<Product> searchProducts(@Param("search") String search, Pageable pageable);
// SecureController.java - Input validation
@GetMapping("/products/search")
public Page<?> searchProductsSecure(
@RequestParam
@Size(min = 1, max = 100)
@Pattern(regexp = "^[a-zA-Z0-9\\s\\-]+$")
String name,
Pageable pageable) {
return productRepository.searchProducts(name, pageable);
}- Use parameterized queries (PreparedStatement, JPA @Query)
- Validate and sanitize all input
- Use ORM frameworks properly
- Apply least privilege to database accounts
- Use allowlists for input validation
Missing security controls in the design phase - no rate limiting, no abuse prevention.
// AuthService.java - Account lockout after failed attempts
private static final int MAX_FAILED_ATTEMPTS = 5;
private static final int LOCK_DURATION_MINUTES = 30;
private void handleFailedLogin(User user, HttpServletRequest request) {
int newFailedAttempts = user.getFailedLoginAttempts() + 1;
userRepository.updateFailedLoginAttempts(user.getEmail(), newFailedAttempts);
if (newFailedAttempts >= MAX_FAILED_ATTEMPTS) {
userRepository.lockUser(user.getEmail(), false, LocalDateTime.now());
auditLogService.logAccountLocked(user.getEmail(), request);
}
}- Threat modeling during design
- Rate limiting on sensitive endpoints
- Account lockout mechanisms
- CAPTCHA for public forms
- Principle of least privilege
Weak passwords, no account lockout, credential stuffing vulnerabilities.
// AuthDto.java - Strong password policy
public class RegisterRequest {
@NotBlank
@Email(message = "Invalid email format")
private String email;
@NotBlank
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$",
message = "Password must have 8+ chars, uppercase, lowercase, digit, special char"
)
private String password;
}
// AuthService.java - Login with lockout
public AuthResponse login(LoginRequest request, HttpServletRequest httpRequest) {
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new BadCredentialsException("Invalid credentials"));
// Check if account is locked
if (!user.isAccountNonLocked()) {
if (isLockExpired(user)) {
unlockAccount(user);
} else {
throw new LockedException("Account locked. Try again later.");
}
}
// ... authentication logic
}- Strong password policies
- Account lockout mechanisms
- Multi-factor authentication (MFA)
- Secure password storage (BCrypt)
- Session timeout and management
Unsigned tokens, insecure deserialization, untrusted CI/CD pipelines.
// JwtTokenProvider.java - Signed JWT tokens
public String generateToken(String username) {
return Jwts.builder()
.subject(username)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(getSigningKey()) // HS256 with secure key
.compact();
}
// SecurityConfig.java - CSRF protection for session auth
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))- Sign all tokens and verify signatures
- Use CSRF tokens for session-based auth
- Secure CI/CD pipelines
- Verify software integrity (checksums, signatures)
- Avoid native deserialization
Insufficient logging, no monitoring, unable to detect or respond to breaches.
// AuditLogService.java - Comprehensive security logging
@Service
public class AuditLogService {
public void logLoginSuccess(String username, HttpServletRequest request) {
createAuditLog(AuditEventType.LOGIN_SUCCESS,
"Successful login", username, request, AuditSeverity.INFO);
}
public void logLoginFailure(String username, HttpServletRequest request, String reason) {
createAuditLog(AuditEventType.LOGIN_FAILURE,
"Failed login: " + reason, username, request, AuditSeverity.WARNING);
}
public void logAccessControlViolation(String description, HttpServletRequest request) {
createAuditLog(AuditEventType.ACCESS_CONTROL_VIOLATION,
description, getCurrentUsername(), request, AuditSeverity.CRITICAL);
}
public void logInjectionAttempt(String description, HttpServletRequest request) {
createAuditLog(AuditEventType.INJECTION_ATTEMPT,
description, getCurrentUsername(), request, AuditSeverity.CRITICAL);
}
}View Security Logs: Admin Panel → Security Logs
- Log all security events (login, access, failures)
- Include context (IP, user agent, timestamp)
- Use severity levels for alerting
- Protect logs from tampering
- Set up monitoring and alerting
Exposing stack traces, failing open, improper error handling.
Access /api/owasp/vulnerable/error-demo?action=divide to see:
- Full stack trace
- Class names and methods
- Line numbers
- Internal implementation details
// VulnerableController.java
@GetMapping("/error-demo")
public Map<String, Object> errorDemoInsecure(@RequestParam String action) {
try {
// some operation that throws
} catch (Exception e) {
response.put("error", e.getClass().getName());
response.put("message", e.getMessage());
response.put("stackTrace", getStackTraceAsString(e)); // DANGEROUS!
response.put("cause", e.getCause().toString());
}
return response;
}// GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleAllExceptions(Exception ex) {
// Log full details internally
log.error("Unexpected error", ex);
// Return generic message to user
return ResponseEntity.status(500)
.body(ApiResponse.error("An unexpected error occurred"));
}
}
// SecureController.java
@GetMapping("/error-demo")
public ResponseEntity<?> errorDemoSecure(@RequestParam String action) {
try {
// some operation
} catch (Exception e) {
log.error("Error in demo: {} - {}", e.getClass().getSimpleName(), e.getMessage(), e);
return ResponseEntity.badRequest()
.body(ApiResponse.error("An error occurred processing your request"));
}
}- Never expose stack traces to users
- Log detailed errors internally
- Return generic error messages
- Use global exception handlers
- Fail securely (deny access on error)
| Vulnerability | Vulnerable Endpoint | Secure Endpoint |
|---|---|---|
| A01: Broken Access Control | GET /api/owasp/vulnerable/orders/{id} |
GET /api/orders/{id} |
| A02: Security Misconfiguration | GET /api/owasp/vulnerable/config |
GET /api/owasp/secure/config |
| A04: Cryptographic Failures | GET /api/owasp/vulnerable/users/{id} |
GET /api/owasp/secure/users/me |
| A05: Injection | GET /api/owasp/vulnerable/products/search?name=... |
GET /api/products/search?query=... |
| A10: Exception Handling | GET /api/owasp/vulnerable/error-demo?action=divide |
GET /api/owasp/secure/error-demo?action=divide |
The application implements these security headers:
// SecurityConfig.java
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.deny()))