Series MapLesson 05 / 35
Start HereOrdered learning track

Learn Java Security Cryptography Integrity Part 005 Secure Coding Boundaries Input Output Canonicalization

20 min read3996 words
PrevNext
Lesson 0535 lesson track0106 Start Here

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 menerima role=USER,ADMIN karena 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:

  1. sumber data,
  2. representasi data,
  3. normalisasi/canonicalization,
  4. validasi domain,
  5. transformasi,
  6. sink/context tempat data digunakan,
  7. 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-skillPertanyaan praktis
Source identificationDari mana data berasal: user, file, header, queue, DB, config, env, partner API?
Representation awarenessApakah data masih encoded, sudah decoded, normalized, parsed, atau canonical?
Canonicalization disciplineApakah validasi dilakukan terhadap bentuk final yang benar?
Domain validationApakah nilai memenuhi domain bisnis dan bukan hanya regex teknis?
Contextual output encodingData keluar ke HTML, JavaScript, SQL, shell, log, URL, XML, JSON, LDAP, atau file path?
Parser hardeningApakah parser dibatasi terhadap entity expansion, oversized input, polymorphic payload, recursion, dan ambiguous grammar?
Sink controlApakah nilai yang sudah lolos validasi tetap aman ketika masuk ke sink berbahaya?
Failure semanticsJika 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.

SourceContohRisiko
HTTP requestbody, query, path variable, header, cookieinjection, auth bypass, parser confusion, smuggling
Browser-controlled contextOrigin, Referer, User-Agent, Accept-Languagespoofing, policy bypass jika dipercaya
File uploadfilename, content-type, bytes, metadatamalware, decompression bomb, path traversal, parser exploit
Message brokerKafka/RabbitMQ/PubSub payloadpoison message, replay, schema drift, forged producer
Databasestored user content, imported datasecond-order injection, XSS, stale trust decision
Environment/configenv vars, property files, feature flagsconfig injection, secret leak, unsafe default
Partner APIwebhook, sync data, SFTP importtrust transitivity, replay, malformed records
Logs/observabilityMDC fields, trace IDs, error messageslog injection, sensitive data leak
Internal serviceservice-to-service requestconfused 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

InvariantTempat yang tepat
Format syntaxboundary parser / DTO validation
Domain formatvalue object constructor/factory
Authorizationservice/application layer close to use-case
Cross-record consistencytransaction/application service
Output safetyoutput context renderer/encoder
Persistence constraintsDB 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:

BentukMakna potensial
%2e%2e%2f../ setelah URL decode
..\parent directory di Windows
%252e%252e%252fdouble-encoded traversal
example.comUnicode full-width character
example.com.DNS absolute form
127.000.000.001localhost 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

DimensiContoh
Presencerequired vs optional
Typeinteger, UUID, ISO date, enum
Rangeamount >= 0, date not in future
Lengthmax 2000 chars, max 10MB payload
Charset/controlreject null byte, control char jika tidak valid
Formatemail-like, case-id, reference-id
Domainstatus transition valid, actor belongs to tenant
Cardinalitymax 100 IDs per request
Temporalrequest timestamp within 5 minutes
Relationalcase.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:

DomainTipe yang lebih aman
ID internalUUID, custom CaseId, custom TenantId
Date/timeInstant, LocalDate, ZonedDateTime dengan policy jelas
MoneyBigDecimal + currency object, bukan double
URIURI setelah allowlist scheme/host/port/path
Email-ishdomain-specific parser, jangan hanya regex RFC ekstrem
Role/statusenum + explicit transition table
Permissionvalue object/policy expression
File pathPath 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/contextSafe strategy
SQLprepared statement / parameter binding
HTML bodyHTML text encoding / framework auto-escape
HTML attributeattribute-specific encoding, quote attributes
JavaScriptavoid inline JS; otherwise JS-string encoding
CSSavoid dynamic CSS; strict allowlist
URL query paramURL component encoding
XMLXML escaping + parser hardening
JSONserializer, not string concatenation
Shellavoid shell; use ProcessBuilder args array if unavoidable
Logstructured logging + CR/LF sanitization
LDAPLDAP 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.

HeaderRisiko
Hosthost header injection, password reset poisoning
X-Forwarded-Forspoofed IP jika proxy chain tidak dikontrol
X-Forwarded-Protofalse secure/insecure scheme decision
OriginCSRF/CORS decision jika parsing salah
Refererprivacy leak, spoof/absence
User-Agentlog injection, parser bugs
Accept-Languagelocale 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

BoundaryTest negatif
ID parsernull, empty, overlong, Unicode confusable, invalid prefix
URL parserlocalhost, private IP, redirect, userinfo, non-HTTPS, punycode
Path resolver../, encoded traversal, absolute path, symlink, Windows separator
JSON parserunknown fields, duplicate keys, deep nesting, huge arrays
XML parserexternal entity, DTD, entity expansion, external schema
Regexworst-case input, overlong input
Log fieldCR/LF, secret-like value, JSON-breaking characters
Date parsertimezone 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.ROOT untuk 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 modeGejalaPencegahan
Validate-before-decodeencoded payload bypassdecode/canonicalize sebelum validation
Stringly typed domainraw string masuk service dalamvalue object/factory parser
Context confusionvalid data menjadi XSS/log injectionoutput encoding per sink
Parser default trustXXE/deserialization bughardening parser, safe defaults
Header trust mistakespoofed IP/scheme/hosttrusted proxy policy
SSRF by redirectURL awal aman, redirect internalvalidate every redirect, egress controls
Over-strict human validationuser valid ditolakvalidate security properties, encode output
Silent fallbackunknown action jadi defaultfail closed
Logging raw inputforged logs/secrets leakstructured 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

  1. Apakah tenantId dari path cocok dengan tenant actor?
  2. Apakah X-Request-Id boleh ditentukan client atau harus generated?
  3. Apakah filename dipakai sebagai storage key?
  4. Apakah CSV parser punya limit row/column/size?
  5. Apakah CSV cell dapat memicu formula injection saat diekspor kembali?
  6. Apakah error row-level leak data tenant lain?
  7. Apakah import idempotent?
  8. Apakah dry-run tetap melakukan write ke temp storage?
  9. Apakah audit log mencatat hash file, actor, tenant, result?
  10. 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 413 or 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:

  1. Bentuk data apa yang sedang divalidasi? encoded, decoded, normalized, parsed, canonical?
  2. Siapa yang mengontrol data ini? user, partner, internal job, DB, config?
  3. Keputusan security apa yang dibuat dari data ini? authz, path, URL, SQL, rendering, routing?
  4. Interpreter apa yang akan menerima data ini? SQL, HTML, shell, regex, XML, template, log, HTTP client?
  5. Apa failure mode jika parser berikutnya punya interpretasi berbeda?
  6. Apakah tipe domain mencegah misuse setelah boundary?
  7. 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:

  1. Data tidak aman karena berasal dari layer internal; data aman hanya untuk claim yang terbukti.
  2. Validasi harus dilakukan pada representasi yang benar.
  3. Decode/canonicalize discipline lebih penting daripada regex panjang.
  4. Input validation tidak menggantikan output encoding.
  5. Setiap sink punya aturan escaping/parameterization sendiri.
  6. Parser default harus direview.
  7. String mentah harus cepat diubah menjadi tipe domain.
  8. Log adalah sink security.
  9. SSRF dan path traversal adalah masalah capability, bukan hanya masalah string.
  10. 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.
Lesson Recap

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.