Gray-Box Security Notes for WebGoat 8: Defensive Patterns and Secure Implementations
SQL Injection
-
Risk indicators
- String-concatenated predicates, e.g., building WHERE clauses from raw request parameters.
- Dynamic DDL/DCL powered by user input (ALTER, GRANT, DROP).
- Client-provided sort keys fed directly into ORDER BY.
-
Safer patterns (Java/JDBC)
try (var conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PW);
var ps = conn.prepareStatement("SELECT department FROM employees WHERE first_name = ?")) {
ps.setString(1, firstName);
try (var rs = ps.executeQuery()) {
// consume results
}
}
-
Avoid privileged statements from untrusted input
- Never map user input to DDL/DCL; restrict to least-privilege service acounts.
-
Sorting with allow-lists
// Accept only vetted columns for ORDER BY (cannot parameterize identifiers)
var allowed = Map.of("ip", "ip", "hostname", "hostname", "mac", "mac");
var requested = request.getParameter("column");
var column = allowed.get(requested);
if (column == null) throw new IllegalArgumentException("Unsupported sort key");
var sql = "SELECT id, ip, hostname, mac FROM servers ORDER BY " + column;
try (var ps = conn.prepareStatement(sql); var rs = ps.executeQuery()) {
// ...
}
- Application-level protections
- Cenrtalize query construction; prohibit ad‑hoc SQL building in controller code.
- Employ ORM/DSLs with parameter binding; enable query logging and anomaly detection.
- Use database accounts with no schema-alter permissions.
SQL Injection Mitigations (Implementation Notes)
- Use try-with-resources and strict typing
try (var conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PW);
var ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?")) {
ps.setString(1, userName);
try (var rs = ps.executeQuery()) {
while (rs.next()) {
// ...
}
}
} catch (SQLException e) {
// log with structured logging, do not reveal SQL or stack traces to clients
}
-
Validate inputs before hitting the database
- Enforce length/charset/format constraints.
- Prefer server-side allow-lists to reject unexpected values early.
-
Defensive ORDER BY and field selection
record SortSpec(String column, boolean asc) {}
static SortSpec parseSort(HttpServletRequest req) {
var allowedCols = Set.of("created_at", "username", "status");
var col = req.getParameter("sort");
var dir = req.getParameter("dir");
if (!allowedCols.contains(col)) throw new IllegalArgumentException("bad sort");
var asc = !"desc".equalsIgnoreCase(dir);
return new SortSpec(col, asc);
}
Authentication and JWT
- Signature verification
- Reject tokens with unexpected algorithms; do not except "none".
- Match algorithm to expected key type (e.g., HS256 with shared secret or RS256 with public key).
DecodedJWT decoded = JWT.decode(token);
if (!"HS256".equals(decoded.getAlgorithm())) {
throw new SecurityException("Unexpected JWT alg");
}
Algorithm algo = Algorithm.HMAC256(hmacSecret);
JWTVerifier verifier = JWT.require(algo)
.withAudience("webgoat.org")
.acceptLeeway(2)
.build();
verifier.verify(token);
- Key management and the kid header
- Do not interpolate kid values into SQL/paths.
String kid = decoded.getKeyId();
String sql = "SELECT secret FROM jwt_keys WHERE id = ?";
String secret = jdbc.queryForObject(sql, String.class, kid);
- Operational safeguards
- Strong secrets (≥256 bits for HMAC), rotation, and short exp.
- Enforce audience/issuer checks; maintain allow‑listed algs per client.
Password Reset
- Token design
- Single-use, high-entropy, time-limited; bind to user and purpose.
SecureRandom sr = new SecureRandom();
byte[] buf = new byte[32];
sr.nextBytes(buf);
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
// Persist: {userId, tokenHash, expiresAt, used=false}
- URL construction
- Build absolute URLs from server configuration; never trust Host/X‑Forwarded‑* headers without a vetted proxy.
- Validate token server-side; do not rely on predictable or derived values.
XXE (XML External Entity)
- Secure parser configuration
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
- Prefer JSON for transport; if XML is required, enforce strict schemas and size limits.
Insecure Direct Object References (IDOR)
- Enforce object-level authorization
Order order = orderRepo.findById(orderId)
.orElseThrow(() -> new NotFoundException());
if (!order.getOwnerId().equals(currentUser.getId())) {
throw new AccessDeniedException("forbidden");
}
return order;
- Design guidelines
- Use opaque, non-predictable identifiers where possible.
- Apply checks consistently on every endpoint, not just views.
Missing Function-Level Access Control
- Method-level enforcement
@RestController
@RequestMapping("/admin")
@PreAuthorize("hasAuthority('ADMIN')")
class AdminController {
@GetMapping("/config")
public Config view() { /* ... */ }
}
- Validate Content-Type and accept headers strictly; do not expose alternate behaviors on the same path without authorization gates.
Cross-Site Scripting (XSS)
- Output encoding and templating
<!-- Server-side template engine (e.g., Thymeleaf) -->
<p th:text="${cardNumber}"></p> <!-- Escaped by default -->
- Client-side insertion
document.querySelector('#output').innerText = userInput; // never innerHTML
- Complementary defenses: CSP, HTTPOnly cookies, input length limits.
Insecure Deserialization
- Avoid native Java serialization for untrusted data
- Prefer JSON/Protobuf with explicit schemas.
- If deserialization is unavoidable, apply type filtering and sandboxing.
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("maxbytes=10240;!java.lang.Runtime;!*script*;!*");
try (var ois = new ObjectInputStream(in)) {
ois.setObjectInputFilter(filter);
Object obj = ois.readObject();
}
- For JSON, disable polymorphic typing unless strictly required
ObjectMapper om = new ObjectMapper();
om.disable(MapperFeature.DEFAULT_VIEW_INCLUSION);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
// Or better: avoid default typing; register concrete types only
Vulnerable Components
- Track and patch dependencies
<!-- Maven: OWASP Dependency-Check -->
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>9.0.0</version>
<executions>
<execution>
<goals><goal>check</goal></goals>
</execution>
</executions>
</plugin>
- Use SBOMs and CI policies to block builds with known CVEs.
Cross-Site Request Forgery (CSRF)
- Tokenization and SameSite
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.requiresChannel(ch -> ch.anyRequest().requiresSecure());
- For JSON APIs
- Require a CSRF token header from same-origin pages, or rely on SameSite=strict and double-submit cookies.
- Validate Origin/Referer for state-changing requests.
Server-Side Request Forgery (SSRF)
- Allow-list and network egress controls
URI uri = URI.create(userUrl);
if (!Set.of("https", "http").contains(uri.getScheme())) throw new IllegalArgumentException();
InetAddress addr = InetAddress.getByName(uri.getHost());
if (addr.isAnyLocalAddress() || addr.isLoopbackAddress() || isPrivate(addr)) {
throw new AccessDeniedException("internal address");
}
// perform request via hardened HTTP client with timeouts
static boolean isPrivate(InetAddress a) {
byte[] b = a.getAddress();
int first = b[0] & 0xFF;
int second = b[1] & 0xFF;
return first == 10 || (first == 172 && (second >= 16 && second <= 31)) || (first == 192 && second == 168);
}
- Resolve DNS per request and re-check IP after redirects; deny file://, gopher://, etc.
Bypassing Front-End Restrictions (Defensive View)
- Never trust client-side validation; validate on the server using schemas and annotations
record Registration(
@NotBlank @Size(max = 40) String username,
@Email String email,
@Pattern(regexp = "^[0-9]{10}$") String phone
) {}
Client-Side Filtering
- Treat client-side filters as UX only; enforce authorization and data shape server-side.
- Avoid exposing internal APIs that return unrestricted data; implement pagination and field selection allow-lists.
HTML Tampering
- Recompute sensitive fields on the server
BigDecimal serverTotal = items.stream()
.map(i -> i.price().multiply(BigDecimal.valueOf(i.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (requestTotal.compareTo(serverTotal) != 0) {
throw new IllegalStateException("mismatched total");
}
Operational Hardening Summary
- Centralize input validation and encoding.
- Apply RBAC checks at controller and service layers.
- Log security-relevant events and enable rate limits.
- Conduct dependency and container image scanning.
- Use secure defaults (HTTP security headers, TLS-only) and enforce least privilege across the stack.