skip to content

Secure Coding on the JVM (Concept Bridge)

The mapping from general secure-coding rules to concrete JVM mechanics: PreparedStatement for SQL injection, ProcessBuilder argument arrays for command injection, canonical-path checks for traversal, Bean Validation for input, and char arrays for secrets. This is the checklist interviewers expect when they ask how you would harden Java code.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

How does Java prevent SQL injection, and why does string concatenation of SQL fail to do so?

level: juniorimportance: must knowfreq 88%

answer

  1. Concatenation = input parsed as code
  2. ? placeholder / :named param = data, not code
  3. DB compiles template before seeing the value
  4. Identifiers can't be bound -> allow-list
  5. Plan caching is a free bonus

basics

~20 s

Never build SQL by gluing user input into the query text. Use PreparedStatement with ? placeholders (or JPA named parameters) and bind values separately, so input is treated as data, not as code the database runs.

solid answer

~40 s

SQL injection happens when attacker-controlled text is concatenated into a SQL string, so the database parses parts of that input as SQL syntax (e.g. "' OR '1'='1"). The fix is parameterized queries: a PreparedStatement sends the query template with ? placeholders to the database first, the database compiles that fixed structure, and then you bind values via setString/setLong. Bound values are transported out-of-band as pure data and can never change the parsed query shape, regardless of quotes or keywords inside them. In JPA/Hibernate you use named parameters (:name with setParameter) for the same effect; never the string-concatenation form of createQuery. Beware false safety: PreparedStatement does not parameterize identifiers (table/column names) or LIMIT in some drivers, so those must be validated against an allow-list. Parameterization also lets the DB cache the execution plan.

code

java · 15 lines
java
// VULNERABLE: input becomes part of the parsed SQL
String bad = "SELECT * FROM users WHERE name = '" + name + "'";
stmt.executeQuery(bad);

// SAFE: structure compiled first, value bound as data
String sql = "SELECT * FROM users WHERE name = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setString(1, name);
    try (ResultSet rs = ps.executeQuery()) { /* ... */ }
}

// SAFE (JPA named parameter)
em.createQuery("SELECT u FROM User u WHERE u.name = :name", User.class)
  .setParameter("name", name)
  .getResultList();

go deeper

for a junior

States the rule: never concatenate user input into SQL; use PreparedStatement / JPA named parameters.

for a middle

Explains the data-vs-code mechanism (template compiled before values are bound) and that ORMs are safe only when not string-concatenating JPQL.

for a senior

Knows the limits: identifiers/sort can't be bound (allow-list them), LIKE wildcards, manual escaping is wrong, plus the plan-cache benefit.

for a principal

Frames it as a trust-boundary discipline enforced systemically (static analysis, banning concatenated query APIs, least-privilege DB accounts) rather than per-call vigilance.

## The vulnerability **SQL injection (SQLi)** is when an attacker manipulates the SQL a program sends to its database by smuggling SQL syntax inside what was supposed to be plain data (a username, a search term). It is one of the oldest and most damaging web vulnerabilities — it can read, modify, or delete any data the app's DB account can touch. **Why it happens.** A database receives SQL as *text* and parses it into a query plan. If you build that text by string concatenation, the database cannot tell which characters came from the developer and which came from the user — it just parses the whole string. Example of the broken pattern: ```java String sql = "SELECT * FROM users WHERE name = '" + name + "'"; ``` If `name` is `x' OR '1'='1`, the executed SQL becomes `... WHERE name = 'x' OR '1'='1'`, which is always true and returns every row. If `name` is `x'; DROP TABLE users; --`, the input has injected an entirely new statement. The single quote in the input *broke out* of the string literal — this is the essence of an **injection**: data crossing a **trust boundary** (untrusted outside → trusted query) gets reinterpreted as code. ## The fix: parameterized (prepared) queries A **PreparedStatement** separates the query's *structure* from its *values*: ```java String sql = "SELECT * FROM users WHERE name = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, name); // value bound separately try (ResultSet rs = ps.executeQuery()) { /* ... */ } } ``` The `?` is a **placeholder** (a *bind parameter*). The driver/database compiles the template `WHERE name = ?` *before* it ever sees `name`'s value. The value is then sent over the wire as a typed parameter, not spliced into the SQL text. Because the parsed query shape is already fixed, no quote, semicolon, or keyword inside the value can alter it — the value `x' OR '1'='1` is treated as a literal name to look up, and simply matches nothing. This is **data-vs-code separation**: the defense works at the protocol level, not by trying to clean the input. **JPA / Hibernate** give the same guarantee with **named parameters**: ```java List<User> users = em.createQuery( "SELECT u FROM User u WHERE u.name = :name", User.class) .setParameter("name", name) .getResultList(); ``` Spring Data derived queries (`findByName`) and `NamedParameterJdbcTemplate` are parameterized under the hood. The thing to *never* do is build the JPQL/SQL by concatenation. ## Important limits (so you don't get a false sense of safety) - **Identifiers are not parameterizable.** You cannot bind a table name, column name, or sort direction with `?`. If those are dynamic, validate them against a fixed **allow-list** of permitted values; never concatenate user text there. - **`LIKE` wildcards** (`%`, `_`) inside a bound value are still wildcards; escape them if the user shouldn't control matching breadth — but this is a correctness/abuse issue, not classic SQLi. - **Manual escaping is not a substitute.** Trying to escape quotes yourself is fragile (encodings, multibyte tricks) and is the wrong mental model. Parameterize. ## Bonus Parameterized queries also let the database **cache and reuse the execution plan** across calls with different values, which is a performance win, and they avoid locale/format bugs from converting values to strings yourself.

  • You need a dynamic column to sort by from the UI. PreparedStatement can't bind it. What do you do?
    Validate the requested column and direction against a fixed allow-list of known-safe identifiers and map to a literal in code; never concatenate the raw user value into the SQL.
  • Is a stored procedure automatically safe from SQL injection?
    No. A stored procedure that builds and EXECUTEs dynamic SQL by concatenating its parameters is just as injectable. Safety comes from parameterized execution, wherever the SQL is assembled.

saying these in an interview costs you the question

  • Claiming manual quote-escaping or input sanitizing is an equivalent fix
  • Thinking PreparedStatement can parameterize table/column names or ORDER BY direction
  • Believing an ORM is automatically safe even with string-concatenated JPQL
  • Confusing 'prepared' (plan caching) with the security property — security comes from binding, not preparing

context

open as a page

How do you prevent path traversal in Java when building a file path from user-supplied input?

level: middleimportance: must knowfreq 70%

basics

~10 s

Don't trust the input filename. Resolve the full real path (Path.normalize or File.getCanonicalPath), then check it actually stays inside your intended base directory before opening it. Reject anything that escapes, like ../ sequences.

open as a page

What role does Bean Validation (JSR-380) play in securing a Java application, and what are its limits as a security control?

level: middleimportance: should knowfreq 55%

basics

~20 s

Bean Validation (annotations like @NotNull, @Size, @Pattern, @Email on fields, triggered by @Valid) checks that incoming data has the expected shape at the edge of your app. It reduces bad/malicious input but does not replace output-side defenses like parameterized SQL or encoding.

open as a page

Why is ProcessBuilder with an argument array safer than Runtime.exec with a shell string for running external programs?

level: middleimportance: should knowfreq 62%

basics

~20 s

Passing a single command string to a shell lets attacker input add extra commands (via ; | && etc.). ProcessBuilder with a list of separate arguments runs the program directly with no shell, so each argument stays one literal value.

open as a page

Why are passwords and secrets handled as char[] rather than String in Java, and how do you clear them?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Strings in Java are immutable and you can't erase them, so a secret stays in memory until garbage collection happens, exposed in heap dumps. A char[] is mutable, so you can overwrite it with Arrays.fill right after use to shorten the exposure.

open as a page