Learn Java Security Cryptography Integrity Part 005 Secure Coding Boundaries Input Output Canonicalization
title: Learn Java Security, Cryptography and Integrity - Part 005 description: Secure coding boundaries for Java systems: input validation, canonicalization, output encoding, parser boundaries, path and file boundaries, Unicode pitfalls, SSRF boundaries, and dangerous defaults. series: learn-java-security-cryptography-integrity seriesTitle: Learn Java Security, Cryptography and Integrity order: 5 partTitle: Secure Coding Boundaries, Input/Output, Canonicalization tags:
- java
- security
- secure-coding
- input-validation
- canonicalization
- output-encoding
- injection
- ssrf
- path-traversal
- secure-engineering date: 2026-06-30
Part 005 — Secure Coding Boundaries, Input/Output, Canonicalization
Target bagian ini: mampu mendesain dan mereview boundary code di aplikasi Java sehingga data yang masuk, berubah bentuk, dan keluar dari sistem tetap berada dalam format, domain, dan konteks yang benar.
Banyak vulnerability serius bukan terjadi karena engineer tidak tahu algoritma crypto atau framework auth. Banyak yang terjadi karena sistem salah memahami bentuk data.
Contoh sederhana:
- user mengirim
..%2f..%2fetc%2fpasswd, sistem memvalidasi string mentah, lalu framework melakukan decode setelah validasi; - API menerima
role=USER, lalu layer berikutnya menerimarole=USER,ADMINkarena parser berbeda; - HTML template sudah auto-escape, tetapi data dimasukkan ke JavaScript context tanpa encoding yang benar;
- service menerima URL webhook, melakukan allowlist domain, tetapi resolver DNS mengarah ke metadata service internal;
- XML parser membaca external entity karena default parser tidak dibatasi.
Security boundary bukan hanya if input is valid. Security boundary adalah kombinasi:
- sumber data,
- representasi data,
- normalisasi/canonicalization,
- validasi domain,
- transformasi,
- sink/context tempat data digunakan,
- observability dan failure behavior.
Dalam pendekatan Kaufman, bagian ini adalah deliberate practice untuk membaca kode Java bukan sebagai kumpulan controller/service/repository, tetapi sebagai dataflow antar trust boundary.
1. Kaufman Framing: Sub-skill yang Harus Dikuasai
Skill utama:
Mampu mengenali, membangun, dan menguji boundary code yang membuat input tidak dapat berubah menjadi capability yang tidak sah.
Sub-skill:
| Sub-skill | Pertanyaan praktis |
|---|---|
| Source identification | Dari mana data berasal: user, file, header, queue, DB, config, env, partner API? |
| Representation awareness | Apakah data masih encoded, sudah decoded, normalized, parsed, atau canonical? |
| Canonicalization discipline | Apakah validasi dilakukan terhadap bentuk final yang benar? |
| Domain validation | Apakah nilai memenuhi domain bisnis dan bukan hanya regex teknis? |
| Contextual output encoding | Data keluar ke HTML, JavaScript, SQL, shell, log, URL, XML, JSON, LDAP, atau file path? |
| Parser hardening | Apakah parser dibatasi terhadap entity expansion, oversized input, polymorphic payload, recursion, dan ambiguous grammar? |
| Sink control | Apakah nilai yang sudah lolos validasi tetap aman ketika masuk ke sink berbahaya? |
| Failure semantics | Jika gagal validasi, apakah sistem fail closed, tidak leak detail, dan tetap observable? |
Latihan 20 jam untuk skill ini tidak berupa membaca semua daftar CWE. Praktiknya adalah mengambil 10 endpoint nyata dan menggambar aliran data dari source sampai sink.
2. Mental Model: Boundary Code adalah Firewall Semantik
Network firewall membatasi paket. Boundary code membatasi makna.
Security bug muncul ketika salah satu panah di atas diasumsikan aman padahal tidak.
Contoh:
// Bad mental model: "Sudah valid karena tidak mengandung .."
if (fileName.contains("..")) {
throw new BadRequestException();
}
Path target = uploadDir.resolve(URLDecoder.decode(fileName, UTF_8));
Masalahnya bukan hanya regex buruk. Urutan representasi salah: validasi dilakukan sebelum canonicalization/decoding.
Versi yang lebih benar:
Path base = Paths.get("/srv/app/uploads").toRealPath();
String decoded = URLDecoder.decode(fileName, StandardCharsets.UTF_8);
Path candidate = base.resolve(decoded).normalize();
if (!candidate.startsWith(base)) {
throw new BadRequestException("Invalid path");
}
Tetapi ini pun belum lengkap untuk upload produksi. Masih perlu:
- deny absolute path;
- reject path separator jika hanya filename yang diizinkan;
- enforce generated server-side filename;
- enforce content size;
- validate media type using magic bytes, bukan hanya extension;
- scan malware jika domain menuntut;
- store outside webroot;
- ensure permissions;
- avoid following attacker-controlled symlink;
- audit rejected attempts.
Boundary code yang benar jarang hanya satu if.
3. Source: Semua yang Tidak Dibuat oleh Code Path Saat Ini adalah Input
Kesalahan umum: hanya menganggap HTTP body sebagai input. Dalam sistem Java enterprise, input bisa berasal dari banyak sumber.
| Source | Contoh | Risiko |
|---|---|---|
| HTTP request | body, query, path variable, header, cookie | injection, auth bypass, parser confusion, smuggling |
| Browser-controlled context | Origin, Referer, User-Agent, Accept-Language | spoofing, policy bypass jika dipercaya |
| File upload | filename, content-type, bytes, metadata | malware, decompression bomb, path traversal, parser exploit |
| Message broker | Kafka/RabbitMQ/PubSub payload | poison message, replay, schema drift, forged producer |
| Database | stored user content, imported data | second-order injection, XSS, stale trust decision |
| Environment/config | env vars, property files, feature flags | config injection, secret leak, unsafe default |
| Partner API | webhook, sync data, SFTP import | trust transitivity, replay, malformed records |
| Logs/observability | MDC fields, trace IDs, error messages | log injection, sensitive data leak |
| Internal service | service-to-service request | confused deputy, implicit trust mistake |
Rule:
Data dari database bukan otomatis trusted. Data hanya trusted untuk claim yang dapat dibuktikan oleh boundary yang memasukkannya.
Misalnya customer.displayName yang tersimpan di database tetap harus di-encode saat dirender ke HTML. Database hanya membuktikan bahwa data pernah disimpan, bukan bahwa data aman untuk semua output context.
4. Validate Early, but Not Blindly
Kalimat "validate input early" sering benar, tetapi sering disalahartikan.
Yang benar:
Lakukan parsing, decoding, canonicalization, dan validasi sedekat mungkin dengan boundary, lalu ubah data menjadi tipe domain yang aman.
Yang salah:
Jalankan regex cepat pada string mentah lalu anggap semua layer berikutnya aman.
4.1 Boundary DTO vs Domain Value Object
Jangan biarkan String mentah bergerak jauh ke dalam domain.
Buruk:
public void approveCase(String caseId, String actorId, String reason) {
// many layers later: validate here? maybe forgotten
}
Lebih baik:
public record CaseId(String value) {
public CaseId {
if (value == null || !value.matches("CASE-[0-9]{8}")) {
throw new IllegalArgumentException("Invalid case id");
}
}
}
public record ActorId(UUID value) {
public ActorId {
Objects.requireNonNull(value);
}
}
public record ApprovalReason(String value) {
public ApprovalReason {
Objects.requireNonNull(value);
String normalized = Normalizer.normalize(value, Normalizer.Form.NFC).strip();
if (normalized.length() < 10 || normalized.length() > 2000) {
throw new IllegalArgumentException("Invalid reason length");
}
value = normalized;
}
}
Catatan penting: constructor compact record di Java tidak boleh dianggap sebagai silver bullet. Untuk value object yang butuh transformasi lebih eksplisit, factory method lebih jelas:
public record ApprovalReason(String value) {
public static ApprovalReason parse(String raw) {
Objects.requireNonNull(raw, "reason");
String normalized = Normalizer.normalize(raw, Normalizer.Form.NFC).strip();
if (normalized.length() < 10 || normalized.length() > 2000) {
throw new IllegalArgumentException("Invalid reason length");
}
return new ApprovalReason(normalized);
}
}
4.2 Invariant Placement
| Invariant | Tempat yang tepat |
|---|---|
| Format syntax | boundary parser / DTO validation |
| Domain format | value object constructor/factory |
| Authorization | service/application layer close to use-case |
| Cross-record consistency | transaction/application service |
| Output safety | output context renderer/encoder |
| Persistence constraints | DB constraint as last line of defense |
Validasi bukan satu layer. Validasi adalah sistem pertahanan berlapis yang menjaga invariant di tempat berbeda.
5. Canonicalization: Bentuk Final Sebelum Keputusan Security
Canonicalization berarti mengubah input ke bentuk standar sebelum dipakai untuk keputusan keamanan.
Contoh representasi berbeda untuk maksud yang sama:
| Bentuk | Makna potensial |
|---|---|
%2e%2e%2f | ../ setelah URL decode |
..\ | parent directory di Windows |
%252e%252e%252f | double-encoded traversal |
example.com | Unicode full-width character |
example.com. | DNS absolute form |
127.000.000.001 | localhost dengan representasi alternatif |
http://2130706433/ | integer IPv4 localhost representation di beberapa parser |
Security decision harus dilakukan terhadap representasi yang sama dengan representasi yang akan dipakai sink.
5.1 Anti-pattern: Validate Before Decode
if (input.contains("<script>")) {
reject();
}
String decoded = URLDecoder.decode(input, UTF_8);
render(decoded);
Masalah: attacker dapat mengirim encoded script sehingga validasi tidak melihat bentuk final.
5.2 Anti-pattern: Decode Twice
String once = URLDecoder.decode(input, UTF_8);
String twice = URLDecoder.decode(once, UTF_8);
Double decode dapat mengubah data yang sebelumnya tampak aman menjadi karakter berbahaya. Beberapa vulnerability path traversal muncul dari urutan decode/canonicalize yang tidak konsisten antar layer.
5.3 Pattern: Decode Exactly Once at Boundary
public final class RequestText {
private final String value;
private RequestText(String value) {
this.value = value;
}
public static RequestText fromAlreadyDecodedServletParam(String value) {
// Servlet container usually already decoded request parameters.
String normalized = Normalizer.normalize(value, Normalizer.Form.NFC);
return new RequestText(normalized);
}
public String value() {
return value;
}
}
Prinsipnya bukan selalu panggil URLDecoder. Prinsipnya: ketahui apakah framework sudah decode atau belum.
6. Input Validation: Allowlist, Not Vague Blocklist
OWASP mendorong input validation yang spesifik, terutama allowlist untuk nilai yang punya format atau domain jelas.
Blocklist sering gagal karena attacker punya banyak encoding, Unicode confusables, whitespace, parser edge case, dan grammar alternatif.
6.1 Good Validation is Domain-Specific
Buruk:
if (name.contains("<") || name.contains(">")) reject();
Lebih baik:
private static final Pattern PERSON_NAME =
Pattern.compile("^[\\p{L}][\\p{L} .'-]{0,79}$");
public static String parseDisplayName(String raw) {
String normalized = Normalizer.normalize(raw, Normalizer.Form.NFC).strip();
if (!PERSON_NAME.matcher(normalized).matches()) {
throw new BadRequestException("Invalid display name");
}
return normalized;
}
Tetapi hati-hati: nama manusia nyata sulit divalidasi terlalu ketat. Validasi untuk nama biasanya melindungi length, control character, dan rendering context; bukan memaksa semua budaya mengikuti regex sempit.
6.2 Validation Dimensions
| Dimensi | Contoh |
|---|---|
| Presence | required vs optional |
| Type | integer, UUID, ISO date, enum |
| Range | amount >= 0, date not in future |
| Length | max 2000 chars, max 10MB payload |
| Charset/control | reject null byte, control char jika tidak valid |
| Format | email-like, case-id, reference-id |
| Domain | status transition valid, actor belongs to tenant |
| Cardinality | max 100 IDs per request |
| Temporal | request timestamp within 5 minutes |
| Relational | case.tenantId == actor.tenantId |
Security-grade validation menggabungkan syntax dan semantic constraint.
7. Parsing: Ubah String Menjadi Tipe Aman
Java memberi banyak tipe yang lebih baik daripada String:
| Domain | Tipe yang lebih aman |
|---|---|
| ID internal | UUID, custom CaseId, custom TenantId |
| Date/time | Instant, LocalDate, ZonedDateTime dengan policy jelas |
| Money | BigDecimal + currency object, bukan double |
| URI | URI setelah allowlist scheme/host/port/path |
| Email-ish | domain-specific parser, jangan hanya regex RFC ekstrem |
| Role/status | enum + explicit transition table |
| Permission | value object/policy expression |
| File path | Path resolved against base directory |
7.1 Enum Parsing with Fail-Closed Semantics
public enum CaseAction {
SUBMIT, ASSIGN, APPROVE, REJECT, ESCALATE
}
public static CaseAction parseAction(String raw) {
try {
return CaseAction.valueOf(raw.strip().toUpperCase(Locale.ROOT));
} catch (RuntimeException ex) {
throw new BadRequestException("Unsupported action");
}
}
Jangan diam-diam default ke action aman palsu:
// Bad: attacker can cause confusing fallback behavior.
return CaseAction.SUBMIT;
Fail closed lebih baik daripada fallback ambigu.
7.2 Date Parsing Must Declare Clock Semantics
Buruk:
LocalDateTime submittedAt = LocalDateTime.parse(raw);
LocalDateTime tidak membawa timezone/offset. Untuk event security, audit, replay defense, dan token expiry, gunakan Instant atau offset-aware time.
Instant submittedAt = OffsetDateTime.parse(raw).toInstant();
Untuk business date, LocalDate boleh, tetapi jangan dipakai untuk timestamp security.
8. Output Encoding: Aman untuk Sink Tertentu, Bukan Aman Universal
Input validation tidak menggantikan output encoding.
Data yang valid sebagai nama customer tetap bisa berbahaya jika masuk ke JavaScript string, HTML attribute, URL, log line, shell command, LDAP filter, XPath, atau SQL.
8.1 Contexts are Different
| Sink/context | Safe strategy |
|---|---|
| SQL | prepared statement / parameter binding |
| HTML body | HTML text encoding / framework auto-escape |
| HTML attribute | attribute-specific encoding, quote attributes |
| JavaScript | avoid inline JS; otherwise JS-string encoding |
| CSS | avoid dynamic CSS; strict allowlist |
| URL query param | URL component encoding |
| XML | XML escaping + parser hardening |
| JSON | serializer, not string concatenation |
| Shell | avoid shell; use ProcessBuilder args array if unavoidable |
| Log | structured logging + CR/LF sanitization |
| LDAP | LDAP filter escaping |
8.2 SQL Example
Buruk:
String sql = "select * from cases where status = '" + status + "'";
Baik:
try (PreparedStatement ps = connection.prepareStatement(
"select * from cases where status = ?")) {
ps.setString(1, status.name());
try (ResultSet rs = ps.executeQuery()) {
// map rows
}
}
Prepared statement bukan hanya menghindari quote. Ia memisahkan data dan instruksi untuk SQL interpreter.
8.3 JSON Example
Buruk:
String json = "{\"name\": \"" + name + "\"}";
Baik:
record CustomerResponse(String name) {}
String json = objectMapper.writeValueAsString(new CustomerResponse(name));
Gunakan serializer. Jangan merakit format interpreter dengan concatenation.
9. Log Boundary: Log adalah Sink Security
Log sering dianggap aman karena "hanya internal". Itu salah.
Risiko log:
- log injection via newline;
- forged log entries;
- leakage secret, token, password, API key;
- PII over-retention;
- sensitive stack trace;
- trace ID spoofing;
- SIEM alert bypass karena field shape tidak konsisten.
9.1 Anti-pattern: Logging Raw Input
log.warn("Failed login for username={} reason={}", username, reason);
Ini tampak aman karena parameterized logging, tetapi masih bisa leak data dan membuat log injection jika downstream renderer tidak aman.
9.2 Safer Security Event Logging
log.warn("security_event type={} actorHash={} sourceIp={} outcome={} reasonCode={}",
"LOGIN_FAILED",
actorHash(username),
clientIpPolicy.extract(request),
"DENIED",
"INVALID_CREDENTIALS");
Prinsip:
- jangan log password, token, OTP, secret, raw Authorization header;
- hindari raw user-supplied text untuk event security;
- pakai reason code stabil;
- hash/pseudonymize identifier jika diperlukan;
- strukturkan field agar bisa diaudit;
- sanitize CR/LF untuk field yang tetap harus dicatat.
10. Path and File Boundary
File path adalah boundary yang sering diremehkan.
10.1 Path Traversal Mental Model
Attacker ingin mengubah input yang terlihat seperti filename menjadi capability membaca/menulis file arbitrary.
10.2 Safer Filename Policy
Untuk upload, kebijakan paling aman biasanya:
- tidak menggunakan filename user sebagai path storage;
- filename user hanya metadata display setelah sanitization;
- storage key dibuat server: UUID/content hash;
- object disimpan di bucket/path tenant-scoped;
- download melalui authorization check, bukan direct path;
- content-disposition di-set aman.
public record UploadedObjectKey(String value) {
public static UploadedObjectKey create() {
return new UploadedObjectKey(UUID.randomUUID().toString());
}
}
10.3 Path Check Example
public Path resolveSafeChild(Path baseDir, String rawName) throws IOException {
String name = Normalizer.normalize(rawName, Normalizer.Form.NFC).strip();
if (name.contains("/") || name.contains("\\") || name.isBlank()) {
throw new BadRequestException("Invalid file name");
}
Path base = baseDir.toRealPath(LinkOption.NOFOLLOW_LINKS);
Path target = base.resolve(name).normalize();
if (!target.startsWith(base)) {
throw new BadRequestException("Invalid file path");
}
return target;
}
Catatan: symlink race masih bisa terjadi di desain file system tertentu. Untuk sistem sensitif, gunakan storage service dengan policy terisolasi, generated key, permission ketat, dan hindari operasi file path attacker-controlled.
11. URL, URI, SSRF, and Network Boundary
SSRF terjadi ketika attacker membuat server mengirim request ke lokasi yang attacker pilih.
Contoh fitur rentan:
- import image from URL;
- webhook test call;
- PDF generator fetch external assets;
- XML parser fetch external DTD;
- service health checker menerima URL;
- integration connector menerima endpoint partner.
11.1 Jangan Treat URL sebagai String Biasa
Buruk:
if (url.startsWith("https://trusted.example")) {
httpClient.send(HttpRequest.newBuilder(URI.create(url)).build(), BodyHandlers.ofString());
}
Masalah:
https://trusted.example.attacker.com;- encoded host confusion;
- username/password URL confusion;
- redirect ke internal network;
- DNS rebinding;
- IPv4/IPv6 literal localhost;
- mixed parser behavior.
11.2 Safer URL Policy
public URI parsePartnerEndpoint(String raw) {
URI uri = URI.create(raw.strip());
if (!"https".equalsIgnoreCase(uri.getScheme())) {
throw new BadRequestException("Only https is allowed");
}
if (uri.getUserInfo() != null) {
throw new BadRequestException("User info is not allowed");
}
if (uri.getHost() == null) {
throw new BadRequestException("Host is required");
}
String host = IDN.toASCII(uri.getHost().toLowerCase(Locale.ROOT));
if (!host.equals("api.partner.example")) {
throw new BadRequestException("Unsupported host");
}
int port = uri.getPort();
if (port != -1 && port != 443) {
throw new BadRequestException("Unsupported port");
}
return uri;
}
Untuk SSRF serius, validasi URL saja tidak cukup. Tambahkan:
- egress firewall;
- block private, loopback, link-local, metadata ranges;
- no automatic redirects atau validate setiap redirect;
- DNS resolution policy;
- timeout pendek;
- response size limit;
- allowlist destination;
- separate network identity untuk fetcher service;
- no credential forwarding.
11.3 Redirect Rule
Jika HTTP client mengikuti redirect otomatis, attacker bisa memberikan URL allowlisted yang redirect ke internal address. Validasi harus diterapkan ulang pada setiap redirect destination.
12. Parser Boundary: JSON, XML, YAML, CSV, Template, Regex
Parser mengubah bytes menjadi object. Parser sering menjadi boundary berbahaya karena:
- grammar kompleks;
- parser punya default fitur historis;
- payload bisa sangat besar;
- payload bisa deeply nested;
- parser bisa memuat resource eksternal;
- polymorphic deserialization bisa membuat object tak terduga;
- error message bisa leak detail.
12.1 JSON Boundary
Rule:
- pakai schema/DTO eksplisit;
- disable unknown fields untuk endpoint sensitif jika memungkinkan;
- batasi size body di server/proxy;
- batasi nesting depth jika parser mendukung;
- jangan aktifkan polymorphic deserialization untuk untrusted input;
- validate semantic setelah parse.
public record CreateCaseRequest(
String tenantId,
String subject,
String description
) {}
public CreateCaseCommand toCommand(CreateCaseRequest req) {
return new CreateCaseCommand(
TenantId.parse(req.tenantId()),
CaseSubject.parse(req.subject()),
CaseDescription.parse(req.description())
);
}
DTO bukan domain object. DTO adalah boundary representation.
12.2 XML Boundary
XML punya risiko khusus:
- XXE;
- external DTD;
- entity expansion;
- schema poisoning;
- signature wrapping;
- XPath injection;
- oversized trees.
Contoh hardening JAXP secara konseptual:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
dbf.setExpandEntityReferences(false);
dbf.setNamespaceAware(true);
Detail parser bisa berbeda tergantung implementation. Selalu uji konfigurasi parser yang benar-benar dipakai di runtime.
12.3 YAML Boundary
YAML kuat tetapi berbahaya jika parser mendukung arbitrary object construction. Untuk config internal pun, perlakukan YAML sebagai input sensitif:
- jangan parse YAML untrusted menjadi arbitrary Java objects;
- batasi anchor/alias expansion;
- pakai safe constructor;
- schema-kan format.
12.4 Regex Boundary
Regex bisa menyebabkan ReDoS jika pattern punya catastrophic backtracking.
Buruk:
Pattern.compile("^(a+)+$");
Untuk input attacker-controlled:
- hindari nested quantifier;
- pakai length limit sebelum regex;
- prefer parser deterministik untuk grammar kompleks;
- benchmark worst-case input;
- gunakan timeout di boundary processing jika tersedia di arsitektur.
13. Unicode and Locale Pitfalls
Unicode adalah sumber banyak bug boundary.
13.1 Normalize Before Length/Pattern Decision
String normalized = Normalizer.normalize(raw, Normalizer.Form.NFC);
Tanpa normalisasi, dua string yang terlihat sama bisa berbeda byte/code point.
13.2 Locale-Sensitive Case Conversion
Buruk:
String role = raw.toUpperCase();
Lebih aman untuk identifier/protocol:
String role = raw.toUpperCase(Locale.ROOT);
13.3 Control Characters
Untuk field yang tidak membutuhkan control character, reject:
boolean hasControl = normalized.chars()
.anyMatch(ch -> Character.isISOControl(ch) && ch != '\n' && ch != '\t');
Tapi jangan sembarang reject semua non-ASCII untuk data manusia. Security boundary harus mempertahankan usability dan global correctness.
14. Header, Cookie, and Request Metadata Boundary
Header HTTP sering tampak teknis, tetapi banyak yang attacker-controlled.
| Header | Risiko |
|---|---|
Host | host header injection, password reset poisoning |
X-Forwarded-For | spoofed IP jika proxy chain tidak dikontrol |
X-Forwarded-Proto | false secure/insecure scheme decision |
Origin | CSRF/CORS decision jika parsing salah |
Referer | privacy leak, spoof/absence |
User-Agent | log injection, parser bugs |
Accept-Language | locale behavior, cache key explosion |
Rule:
- trust proxy headers hanya dari trusted proxy;
- normalize request scheme/host di edge;
- jangan membuat security decision dari header yang tidak punya provenance;
- log metadata secara terbatas;
- limit header size.
15. Error Boundary: Jangan Leak Internal State
Boundary failure harus jelas bagi client tetapi tidak memberi attacker peta internal.
Buruk:
{
"error": "java.sql.SQLSyntaxErrorException near 'admin' at line 1"
}
Lebih baik:
{
"error": "invalid_request",
"message": "Request could not be processed",
"correlationId": "01J..."
}
Internal log boleh menyimpan detail secukupnya, tetapi harus redacted dan access-controlled.
16. Boundary Testing: Negative Test as First-Class Test
Security boundary tidak cukup diuji happy path.
16.1 Test Matrix
| Boundary | Test negatif |
|---|---|
| ID parser | null, empty, overlong, Unicode confusable, invalid prefix |
| URL parser | localhost, private IP, redirect, userinfo, non-HTTPS, punycode |
| Path resolver | ../, encoded traversal, absolute path, symlink, Windows separator |
| JSON parser | unknown fields, duplicate keys, deep nesting, huge arrays |
| XML parser | external entity, DTD, entity expansion, external schema |
| Regex | worst-case input, overlong input |
| Log field | CR/LF, secret-like value, JSON-breaking characters |
| Date parser | timezone missing, leap edge, far future, expired timestamp |
16.2 Example: Path Traversal Test
@ParameterizedTest
@ValueSource(strings = {
"../secret.txt",
"..\\secret.txt",
"%2e%2e%2fsecret.txt",
"/etc/passwd",
"",
" "
})
void rejectsUnsafeFileNames(String raw) {
assertThrows(BadRequestException.class, () -> resolver.resolveSafeChild(baseDir, raw));
}
16.3 Example: SSRF Test Cases
@ParameterizedTest
@ValueSource(strings = {
"http://api.partner.example/resource",
"https://api.partner.example.attacker.test/resource",
"https://127.0.0.1/",
"https://[::1]/",
"https://169.254.169.254/latest/meta-data/",
"https://user:pass@api.partner.example/resource"
})
void rejectsUnsafePartnerUrls(String raw) {
assertThrows(BadRequestException.class, () -> parser.parsePartnerEndpoint(raw));
}
17. Java Framework Boundary Notes
17.1 Spring MVC / Jakarta REST
Framework sering sudah melakukan:
- URL decoding;
- body parsing;
- content negotiation;
- parameter binding;
- validation annotation;
- exception mapping.
Tapi framework tidak otomatis tahu:
- business invariant;
- tenant boundary;
- safe output context;
- SSRF destination policy;
- file storage policy;
- log sensitivity;
- parser hardening untuk custom parser;
- authorization semantics.
17.2 Bean Validation is Not Enough
@NotBlank, @Size, @Pattern berguna, tetapi sering hanya syntax-level.
public record SubmitCaseRequest(
@NotBlank @Size(max = 120) String subject,
@NotBlank @Size(max = 5000) String description
) {}
Tetap perlu mapping ke value object:
SubmitCaseCommand command = new SubmitCaseCommand(
ActorId.from(authenticatedPrincipal),
TenantId.parse(requestTenant),
CaseSubject.parse(req.subject()),
CaseDescription.parse(req.description())
);
17.3 Database Constraints are Last Line of Defense
DB constraint bagus untuk menjaga integrity, tetapi jangan menjadikan DB sebagai satu-satunya validation layer untuk input attacker-controlled. Error DB bisa leak detail, mempersulit UX, dan terlambat untuk mencegah sink lain seperti log, event, atau downstream call.
18. Pattern: Boundary Adapter
Untuk endpoint kompleks, gunakan boundary adapter yang eksplisit.
final class SubmitCaseHttpAdapter {
private final SubmitCaseUseCase useCase;
HttpResponse handle(HttpRequest request) {
AuthenticatedActor actor = authenticate(request);
SubmitCaseRequest dto = parseJson(request.body());
SubmitCaseCommand command = new SubmitCaseCommand(
actor.actorId(),
TenantId.parse(request.pathParam("tenantId")),
CaseSubject.parse(dto.subject()),
CaseDescription.parse(dto.description()),
RequestId.parseOrGenerate(request.header("X-Request-Id"))
);
SubmitCaseResult result = useCase.submit(command);
return render(result);
}
}
Manfaat:
- source jelas;
- parsing jelas;
- domain conversion jelas;
- auth actor eksplisit;
- boundary test bisa fokus;
- service layer tidak menerima string liar.
19. Secure Coding Boundary Checklist
Gunakan checklist ini saat review PR.
19.1 Source Checklist
- Semua source input diidentifikasi.
- Data dari DB/queue/partner tidak dianggap trusted universal.
- Proxy/header provenance jelas.
- File upload metadata tidak dipercaya.
- Config/env yang memengaruhi security direview.
19.2 Canonicalization Checklist
- Decoding dilakukan tepat sekali di boundary yang dipahami.
- Validasi dilakukan setelah bentuk final/canonical diketahui.
- Tidak ada double decode tersembunyi antar layer.
- Unicode normalization dipakai untuk identifier/text yang relevan.
- Case conversion memakai
Locale.ROOTuntuk protocol/identifier.
19.3 Validation Checklist
- Allowlist dipakai untuk domain yang terbatas.
- Length limit sebelum operasi mahal.
- Domain invariant tidak hanya regex.
- Unknown enum/action fail closed.
- Request cardinality dibatasi.
- Timestamp security memakai
Instant/offset-aware parsing.
19.4 Sink Checklist
- SQL memakai parameter binding.
- JSON/XML memakai serializer, bukan concatenation.
- HTML/JS/URL memakai context-specific encoding.
- Shell dihindari; jika terpaksa, argument array bukan command string.
- Log tidak menyimpan secret/token/raw credential.
- SSRF destination dibatasi dengan allowlist dan egress policy.
19.5 Parser Checklist
- Body size dibatasi.
- JSON unknown/polymorphic behavior disadari.
- XML external entity/schema access dimatikan jika tidak diperlukan.
- Regex worst-case diuji.
- Parser error tidak leak detail internal.
20. Common Failure Modes
| Failure mode | Gejala | Pencegahan |
|---|---|---|
| Validate-before-decode | encoded payload bypass | decode/canonicalize sebelum validation |
| Stringly typed domain | raw string masuk service dalam | value object/factory parser |
| Context confusion | valid data menjadi XSS/log injection | output encoding per sink |
| Parser default trust | XXE/deserialization bug | hardening parser, safe defaults |
| Header trust mistake | spoofed IP/scheme/host | trusted proxy policy |
| SSRF by redirect | URL awal aman, redirect internal | validate every redirect, egress controls |
| Over-strict human validation | user valid ditolak | validate security properties, encode output |
| Silent fallback | unknown action jadi default | fail closed |
| Logging raw input | forged logs/secrets leak | structured sanitized event logging |
21. Mini Lab: Review Endpoint Import File
Bayangkan endpoint:
POST /tenants/{tenantId}/cases/import
Content-Type: multipart/form-data
Input:
- path variable
tenantId; - file upload
cases.csv; - optional query
dryRun=true; - header
X-Request-Id; - authenticated actor dari token.
21.1 Threat Questions
- Apakah
tenantIddari path cocok dengan tenant actor? - Apakah
X-Request-Idboleh ditentukan client atau harus generated? - Apakah filename dipakai sebagai storage key?
- Apakah CSV parser punya limit row/column/size?
- Apakah CSV cell dapat memicu formula injection saat diekspor kembali?
- Apakah error row-level leak data tenant lain?
- Apakah import idempotent?
- Apakah dry-run tetap melakukan write ke temp storage?
- Apakah audit log mencatat hash file, actor, tenant, result?
- Apakah rejected rows tersimpan dengan data sensitif?
21.2 Boundary Design
21.3 Good Acceptance Criteria
- Given invalid tenant ID format, endpoint returns
400. - Given actor from another tenant, endpoint returns
403. - Given file larger than configured limit, endpoint returns
413or domain-specific rejection. - Given filename
../../x, storage still uses server-generated key. - Given CSV with 1M rows, parser stops at configured maximum.
- Given rejected rows, response contains row numbers and reason codes, not raw sensitive values.
- Given successful import, audit event includes actor, tenant, hash, count, and correlation ID.
22. What Top 1% Engineers Do Differently
Top engineers tidak bertanya "apakah sudah divalidasi?" Mereka bertanya:
- Bentuk data apa yang sedang divalidasi? encoded, decoded, normalized, parsed, canonical?
- Siapa yang mengontrol data ini? user, partner, internal job, DB, config?
- Keputusan security apa yang dibuat dari data ini? authz, path, URL, SQL, rendering, routing?
- Interpreter apa yang akan menerima data ini? SQL, HTML, shell, regex, XML, template, log, HTTP client?
- Apa failure mode jika parser berikutnya punya interpretasi berbeda?
- Apakah tipe domain mencegah misuse setelah boundary?
- Apakah test negatif mencakup bypass encoding/canonicalization?
Security boundary adalah tentang menghilangkan ambiguitas representasi.
23. Practice Plan
Hari 1 — Dataflow Reading
Ambil 3 endpoint Java. Untuk tiap endpoint, catat:
- source;
- parser;
- canonicalization;
- validation;
- authz;
- sink;
- output encoding;
- log event.
Hari 2 — Value Object Refactor
Pilih 5 string domain penting dan ubah menjadi value object:
TenantId;CaseId;ActorId;RequestId;ExternalReference.
Hari 3 — Negative Tests
Tambahkan test untuk:
- path traversal;
- overlong input;
- invalid enum;
- Unicode normalization;
- SSRF destination;
- log injection.
Hari 4 — Parser Hardening
Review semua parser non-trivial:
- XML;
- CSV;
- YAML;
- JSON polymorphism;
- template rendering;
- regex.
Hari 5 — PR Review Drill
Ambil PR endpoint baru. Review hanya dengan pertanyaan boundary, bukan style.
24. Ringkasan
Secure coding boundary adalah fondasi Java security yang paling sering menentukan apakah desain bagus benar-benar aman di runtime.
Prinsip inti:
- Data tidak aman karena berasal dari layer internal; data aman hanya untuk claim yang terbukti.
- Validasi harus dilakukan pada representasi yang benar.
- Decode/canonicalize discipline lebih penting daripada regex panjang.
- Input validation tidak menggantikan output encoding.
- Setiap sink punya aturan escaping/parameterization sendiri.
- Parser default harus direview.
- String mentah harus cepat diubah menjadi tipe domain.
- Log adalah sink security.
- SSRF dan path traversal adalah masalah capability, bukan hanya masalah string.
- Negative tests adalah bagian dari design, bukan tambahan QA.
Setelah ini, Part 006 akan membahas secrets, configuration, dan runtime exposure: bagaimana secret hidup, bocor, diputar, disuntikkan ke runtime, dan dikendalikan dalam sistem Java production.
References
- Oracle, Secure Coding Guidelines for Java SE.
- Oracle, Java API for XML Processing Security Guide.
- OWASP Cheat Sheet Series, Input Validation Cheat Sheet.
- OWASP Cheat Sheet Series, Cross Site Scripting Prevention Cheat Sheet.
- OWASP Cheat Sheet Series, Server-Side Request Forgery Prevention Cheat Sheet.
- OWASP Cheat Sheet Series, Secure Code Review Cheat Sheet.
- CWE-180, Incorrect Behavior Order: Validate Before Canonicalize.
- CWE-22, Improper Limitation of a Pathname to a Restricted Directory.
- CWE-35, Path Traversal.
You just completed lesson 05 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.