Java / Advanced / Lesson 17
Secure boundaries and untrusted input
Keep values separate from executable structure, validate resource budgets and domain rules, and protect secrets and diagnostic evidence at Java application boundaries.
On this page
Lesson goals
- Distinguish validation, authorization, and parameterization.
- Bound parsing and reject unsafe deserialization assumptions.
- Use cryptographic and diagnostic tools for their intended purpose.
Define trust and resource limits explicitly
Untrusted input includes files, messages, configuration supplied by another party, and values previously stored without a trusted validation guarantee. Validate type, format, range, size, and allowed state transitions at the appropriate boundary. Validation is not authorization: a well-formed account identifier may still refer to another user's account. Derive access scope from a trusted identity and enforce it consistently.
A domain code can have a strict ASCII format while a person's name legitimately contains Arabic or punctuation. Do not apply one blanket character blacklist to every field. Bound input before expensive parsing, decompression, regular-expression work, or allocation. A parser that accepts the data does not prove the resulting request is permitted or affordable under concurrency.
Validate a domain format without inventing repairs
The example accepts a five-character product code matching two uppercase Latin letters, a hyphen, and two ASCII digits. It rejects null and every other format rather than trimming, uppercasing, or substituting data silently. Such normalization may be valid in another contract, but it must be a deliberate rule with collision handling. This is a product-code policy, not a rule for human names.
Validate lengths before applying a potentially expensive pattern and choose a simple bounded expression. Java's regex engine supports constructs whose work can grow badly on adversarial input, so pattern design and size limits both matter. Return a stable public rejection while retaining only necessary restricted diagnostic context. Never include secrets or full personal records in exception messages just to aid debugging.
public class Main {
static String requireCode(String value) {
if(value==null || value.length()!=5 || !value.matches("[A-Z]{2}-[0-9]{2}"))
throw new IllegalArgumentException("invalid product code");
return value;
}
public static void main(String[] args) throws Exception {
System.out.println(requireCode("AB-12"));
}
}
AB-12
The valid synthetic code is returned unchanged. The method rejects absent or nonmatching values instead of silently inventing a code.
Keep data out of command structure
For JDBC, bind values through PreparedStatement parameters rather than concatenating them into SQL text. Parameters do not substitute table identifiers or sort keywords; those must come from a fixed approved mapping when dynamic structure is required. Parameterization also does not authorize the selected account or make transaction scope correct. Keep these review questions separate.
The fragment assumes an already authorized connection and a validated accountId; it is not a complete runnable application and opens no real connection in this course. Similar care applies to process execution: passing separate ProcessBuilder arguments avoids a shell-parsing boundary only when a shell is not invoked, and the target tool still interprets its own options. Validate permitted operations as well as values.
try (var statement = connection.prepareStatement(
"SELECT display_name FROM account WHERE account_id = ?")) {
statement.setLong(1, accountId);
try (var rows = statement.executeQuery()) {
while (rows.next()) {
System.out.println(rows.getString(1));
}
}
}
connection and accountId are contextual inputs, so this fragment is non-runnable and is never connected to a service.
Use hashes and secrets for their actual guarantees
SHA-256 can calculate a content digest, but an unkeyed digest does not authenticate the sender and a fast general-purpose hash is not a complete password-storage design. The example hashes public synthetic text and formats the result; no secret is generated, stored, or sent. Password handling needs an approved password-hashing design and policy beyond this checksum example.
Use SecureRandom for security-sensitive randomness and avoid logging tokens or keys. Transport security also needs endpoint and certificate validation; disabling validation to suppress a connection error changes the trust boundary. Secret lifecycle includes creation, distribution, rotation, revocation, and access control. An encrypted field is not safe if every component can freely decrypt and print it.
public class Main {
public static void main(String[] args) throws Exception {
byte[] digest=java.security.MessageDigest.getInstance("SHA-256")
.digest("abc".getBytes(java.nio.charset.StandardCharsets.UTF_8));
System.out.println(java.util.HexFormat.of().formatHex(digest));
}
}
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
This verifies deterministic hashing of abc. It is neither a password hash recommendation nor proof of sender authenticity.
Review rich parsers and failure evidence
Do not deserialize arbitrary native Java object streams from untrusted sources. ObjectInputFilter can restrict classes and graph limits for supported use cases, but filtering is one defense rather than permission to accept any object graph. Prefer a simple explicit data format with a schema, size limits, and domain validation. XML and archive processing also need reviewed external-resource and expansion policies.
Security tests should cover unauthorized but well-formed identifiers, oversized inputs, malformed encodings, dangerous command structure, and rejected data without state change. Protect heap dumps, recordings, logs, and exception traces because they can contain sensitive values. A secure API rejects narrowly, leaves state consistent, and provides enough controlled evidence for diagnosis without turning the public response into a disclosure channel.
Common errors and fixes
SQL injection despite valid-looking input
- Why it happens
- Values were concatenated into executable SQL structure.
- How to fix it
- Bind values and independently enforce authorization.
- Prevent it next time
- Review the final JDBC command boundary.
A checksum was treated as sender authentication
- Why it happens
- An unkeyed digest was assigned a guarantee it does not provide.
- How to fix it
- Use an approved authenticated protocol or keyed signature design.
- Prevent it next time
- Document each cryptographic primitive's purpose.
Sensitive data appeared in logs
- Why it happens
- Diagnostic detail crossed the intended disclosure boundary.
- How to fix it
- Restrict, redact, and rotate exposed secrets according to the incident policy.
- Prevent it next time
- Test error responses and logging with synthetic secret markers.
Check your understanding
1. Does a valid accountId and a bound JDBC parameter prove the caller may read it?
Show worked solution
No. Validation proves format or range, and binding preserves value semantics. Authorization must derive permitted scope from trusted identity and verify that the requested account belongs within it. Test a well-formed identifier owned by another user.
2. An import is compressed to 1 MB but expands to 5 GB. Which limit matters?
Show worked solution
Bound both compressed input and expanded data, plus entry count and processing work where relevant. The compressed file size alone does not constrain memory or disk cost. Reject before exceeding the approved resource budget and keep partial state isolated.
Key takeaways
- Validation, binding, authorization, and resource limits are complementary.
- A hash does not automatically authenticate or safely store a password.
- Rich parsing and diagnostic artifacts need explicit trust boundaries.