Skip to content

Latest commit

 

History

History
337 lines (229 loc) · 8.74 KB

java.md

File metadata and controls

337 lines (229 loc) · 8.74 KB

☕ Java — Secure Code Review

A rich set of vulnerable × fixed patterns for Java / Spring. Mapped to the OWASP Top 10:2025.

Jump to: Injection · Deserialization & XXE · Crypto & secrets · Access control · SSRF · Auth · Errors · Mini-case


Injection

SQL injection · A05

❌ Vulnerable:

Statement st = conn.createStatement();
ResultSet rs = st.executeQuery("SELECT * FROM users WHERE email = '" + email + "'");
// JPA:
em.createQuery("FROM User WHERE email = '" + email + "'");

✅ Fixed — PreparedStatement / bound JPQL:

PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE email = ?");
ps.setString(1, email);
em.createQuery("FROM User WHERE email = :email").setParameter("email", email);

Command injection · A05

❌ Vulnerable:

Runtime.getRuntime().exec("ping " + host);

✅ Fixed — arg array, no shell:

new ProcessBuilder("ping", "-c", "1", host).start();

Expression Language / SpEL injection · A05

❌ Vulnerable — evaluating user input as SpEL:

Expression exp = new SpelExpressionParser().parseExpression(userInput);   // RCE
exp.getValue();

✅ Fixed — never parse user input as an expression; if templating is needed, use a restricted context and a fixed template.

Path traversal / Zip Slip · A01

❌ Vulnerable — zip entry name used directly:

File out = new File(destDir, entry.getName());       // "../../etc/cron.d/x"

✅ Fixed — validate the canonical path stays under destDir:

File out = new File(destDir, entry.getName());
if (!out.getCanonicalPath().startsWith(destDir.getCanonicalPath() + File.separator))
    throw new SecurityException("zip slip blocked");

Deserialization & XXE

Native deserialization · A08

❌ Vulnerable:

ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
Object obj = ois.readObject();           // gadget-chain RCE

✅ Fixed — avoid native serialization; fixed-type data format:

MyDto dto = new ObjectMapper().readValue(json, MyDto.class);   // no polymorphic typing

Never enable Jackson activateDefaultTyping() / @JsonTypeInfo on untrusted input.

XXE — XML External Entity · A05

❌ Vulnerable:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Document doc = dbf.newDocumentBuilder().parse(input);

✅ Fixed — disable DTDs / external entities:

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);
Document doc = dbf.newDocumentBuilder().parse(input);

Cryptography & secrets

Weak password hashing · A04

❌ Vulnerable:

String hash = DigestUtils.md5Hex(password);

✅ Fixed — BCrypt (Spring Security):

PasswordEncoder enc = new BCryptPasswordEncoder();
String hash = enc.encode(password);
boolean ok = enc.matches(password, hash);

Hard-coded secrets · A04

❌ Vulnerable:

private static final String API_KEY = "sk_live_...redacted";

✅ Fixed:

@Value("${api.key}")                     // env / Vault / config server
private String apiKey;

Insecure randomness · A04

❌ Vulnerable:

String token = Long.toString(new Random().nextLong());

✅ Fixed — SecureRandom:

byte[] b = new byte[32];
new SecureRandom().nextBytes(b);
String token = HexFormat.of().formatHex(b);

Access control

Missing authorization · A01

❌ Vulnerable:

@GetMapping("/admin/users")
public List<User> all() { return repo.findAll(); }   // anyone

✅ Fixed — method security:

@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/users")
public List<User> all() { return repo.findAll(); }

IDOR · A01

❌ Vulnerable:

@GetMapping("/invoices/{id}")
public Invoice get(@PathVariable Long id) { return repo.findById(id).orElseThrow(); }

✅ Fixed — check ownership against the principal:

@GetMapping("/invoices/{id}")
public Invoice get(@PathVariable Long id, @AuthenticationPrincipal UserDetails me) {
    Invoice inv = repo.findById(id).orElseThrow();
    if (!inv.getOwner().equals(me.getUsername())) throw new AccessDeniedException("nope");
    return inv;
}

SSRF & outbound requests

SSRF · A06

❌ Vulnerable:

InputStream in = new URL(userSuppliedUrl).openStream();

✅ Fixed — allowlist host + reject private ranges:

URI uri = URI.create(userSuppliedUrl);
if (!ALLOWED.contains(uri.getHost())) throw new SecurityException("host not allowed");
InetAddress addr = InetAddress.getByName(uri.getHost());
if (addr.isSiteLocalAddress() || addr.isLoopbackAddress() || addr.isLinkLocalAddress())
    throw new SecurityException("internal blocked");

Open redirect · A01

❌ Vulnerable:

return "redirect:" + request.getParameter("next");

✅ Fixed — allow only relative paths:

String next = request.getParameter("next");
if (next == null || !next.startsWith("/") || next.startsWith("//")) next = "/";
return "redirect:" + next;

Authentication & sessions

CSRF disabled globally · A01

❌ Vulnerable:

http.csrf(csrf -> csrf.disable());       // blanket disable on a session-cookie app

✅ Fixed — keep CSRF for browser flows (disable only for stateless token APIs, deliberately):

http.csrf(Customizer.withDefaults());

Verbose auth errors · A07

❌ Vulnerable:

if (!userExists(u)) throw new RuntimeException("no such user");   // user enumeration

✅ Fixed — generic message for both cases:

if (!authenticate(u, p)) throw new BadCredentialsException("invalid credentials");

Errors & configuration

Leaking stack traces · A10

❌ Vulnerable:

} catch (Exception e) { return ResponseEntity.status(500).body(e.toString()); }

✅ Fixed — log internally, generic response:

} catch (Exception e) {
    log.error("operation failed", e);
    return ResponseEntity.status(500).body("Unexpected error");
}

🧪 Mini-case: Spring controller (before → after)

❌ Vulnerable:

@GetMapping("/report/{id}")
public String report(@PathVariable Long id) {
    Report r = em.createQuery("FROM Report WHERE id = " + id, Report.class)  // A05
                 .getSingleResult();
    log.info("served {} key {}", id, apiKey);                                // A09
    return "<h1>" + r.getTitle() + "</h1>";                                  // A05 XSS
}

✅ Fixed:

@GetMapping("/report/{id}")
public String report(@PathVariable Long id, @AuthenticationPrincipal UserDetails me, Model model) {
    Report r = repo.findById(id).orElseThrow();                              // A05: no raw JPQL
    if (!r.getOwner().equals(me.getUsername())) throw new AccessDeniedException("nope"); // A01
    log.info("served {}", id);                                               // A09
    model.addAttribute("report", r);
    return "report";                                                          // A05: template escapes
}

🔎 Java review shortlist

  • readObject(), Runtime.exec w/ strings, default XML parsers, activateDefaultTyping, SpEL on input → justify or reject.
  • Spring: @PreAuthorize/method security present, CSRF not blindly disabled, actuator endpoints secured.
  • Secrets via @Value/Vault, never in committed application.properties.
  • new Random()/Math.random() for tokens → SecureRandom; Zip Slip guarded on extraction.

📚 References