From 4f825f3441923d54efc59d6fbe0300175b645726 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 01:11:18 +0900 Subject: [PATCH 1/6] test(security): require explicit conversion job authority --- .../ConversionJobTenantAuthorityTest.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/test/java/com/clearfolio/viewer/model/ConversionJobTenantAuthorityTest.java diff --git a/src/test/java/com/clearfolio/viewer/model/ConversionJobTenantAuthorityTest.java b/src/test/java/com/clearfolio/viewer/model/ConversionJobTenantAuthorityTest.java new file mode 100644 index 00000000..cf0a1684 --- /dev/null +++ b/src/test/java/com/clearfolio/viewer/model/ConversionJobTenantAuthorityTest.java @@ -0,0 +1,91 @@ +package com.clearfolio.viewer.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +/** + * Proves that the tenant-aware conversion-job constructor never manufactures + * production authority when explicit tenant or subject claims are absent. + */ +class ConversionJobTenantAuthorityTest { + + @Test + void explicitConstructorRejectsMissingTenantAuthority() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new ConversionJob( + UUID.randomUUID(), + null, + "subject-a", + "report.pdf", + "application/pdf", + "hash", + 10L, + 3 + ) + ); + + assertEquals("tenantId must not be blank", exception.getMessage()); + } + + @Test + void explicitConstructorRejectsBlankTenantAuthorityAfterSanitization() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new ConversionJob( + UUID.randomUUID(), + " \u0000 ", + "subject-a", + "report.pdf", + "application/pdf", + "hash", + 10L, + 3 + ) + ); + + assertEquals("tenantId must not be blank", exception.getMessage()); + } + + @Test + void explicitConstructorRejectsMissingSubjectAuthority() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new ConversionJob( + UUID.randomUUID(), + "tenant-a", + null, + "report.pdf", + "application/pdf", + "hash", + 10L, + 3 + ) + ); + + assertEquals("subjectId must not be blank", exception.getMessage()); + } + + @Test + void explicitConstructorRejectsBlankSubjectAuthorityAfterSanitization() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new ConversionJob( + UUID.randomUUID(), + "tenant-a", + " \u0000 ", + "report.pdf", + "application/pdf", + "hash", + 10L, + 3 + ) + ); + + assertEquals("subjectId must not be blank", exception.getMessage()); + } +} From 4411b1dddce332c7ce86d79b9475c146a6f4939e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 01:15:52 +0900 Subject: [PATCH 2/6] fix(security): require explicit conversion job authority --- .../viewer/model/ConversionJob.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/clearfolio/viewer/model/ConversionJob.java b/src/main/java/com/clearfolio/viewer/model/ConversionJob.java index c5b553ba..d6054c06 100644 --- a/src/main/java/com/clearfolio/viewer/model/ConversionJob.java +++ b/src/main/java/com/clearfolio/viewer/model/ConversionJob.java @@ -82,7 +82,12 @@ public ConversionJob( } /** - * Creates a conversion job with tenant and subject ownership metadata. + * Creates a conversion job with explicit tenant and subject ownership metadata. + * + *

This authority-bearing constructor fails closed when tenant or subject + * claims are absent. Development callers that intentionally use demo + * authority must use one of the convenience constructors that supplies the + * demo identities explicitly.

* * @param jobId job identifier * @param tenantId tenant isolation boundary @@ -104,8 +109,8 @@ public ConversionJob( int maxAttempts ) { this.jobId = jobId; - this.tenantId = normalizeOrDefault(tenantId, DEFAULT_TENANT_ID); - this.subjectId = normalizeOrDefault(subjectId, DEFAULT_SUBJECT_ID); + this.tenantId = requireAuthority(tenantId, "tenantId"); + this.subjectId = requireAuthority(subjectId, "subjectId"); this.originalFileName = sanitize(originalFileName); this.contentType = sanitize(contentType); this.contentHash = contentHash; @@ -125,6 +130,14 @@ private String sanitize(String value) { return value.replace("\u0000", ""); } + private String requireAuthority(String value, String fieldName) { + String sanitized = sanitize(value); + if (sanitized == null || sanitized.isBlank()) { + throw new IllegalArgumentException(fieldName + " must not be blank"); + } + return sanitized.strip(); + } + private String normalizeOrDefault(String value, String fallback) { String sanitized = sanitize(value); if (sanitized == null || sanitized.isBlank()) { From 0670e1660377bcdd26c746324f7bd5a382828285 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 01:16:42 +0900 Subject: [PATCH 3/6] test(security): remove demo fallback expectation for explicit authority --- .../viewer/model/ConversionJobTest.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/test/java/com/clearfolio/viewer/model/ConversionJobTest.java b/src/test/java/com/clearfolio/viewer/model/ConversionJobTest.java index dafeb708..e34f4abb 100644 --- a/src/test/java/com/clearfolio/viewer/model/ConversionJobTest.java +++ b/src/test/java/com/clearfolio/viewer/model/ConversionJobTest.java @@ -76,24 +76,6 @@ void constructorAcceptsExplicitTenantMetadata() { assertFalse(job.belongsToTenant("tenant-b")); } - @Test - void constructorFallsBackToDemoMetadataForBlankTenantClaims() { - ConversionJob job = new ConversionJob( - UUID.randomUUID(), - " \u0000 ", - null, - "report.docx", - "application/octet-stream", - "hash", - 10L, - 3 - ); - - assertEquals(TenantContext.DEMO_TENANT_ID, job.getTenantId()); - assertEquals(TenantContext.DEMO_SUBJECT_ID, job.getSubjectId()); - assertFalse(job.belongsToTenant(null)); - } - @Test void clampsMaxAttemptsToOneWhenZeroIsConfigured() { ConversionJob job = new ConversionJob( From 5b5b4536640570fb53cff6144ca95605df0bf1c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 01:19:33 +0900 Subject: [PATCH 4/6] test(model): restore null and blank tenant comparison coverage --- .../java/com/clearfolio/viewer/model/ConversionJobTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/com/clearfolio/viewer/model/ConversionJobTest.java b/src/test/java/com/clearfolio/viewer/model/ConversionJobTest.java index e34f4abb..44d2ecd1 100644 --- a/src/test/java/com/clearfolio/viewer/model/ConversionJobTest.java +++ b/src/test/java/com/clearfolio/viewer/model/ConversionJobTest.java @@ -55,6 +55,8 @@ void constructorSetsDefaultDemoTenantMetadata() { assertEquals(TenantContext.DEMO_TENANT_ID, job.getTenantId()); assertEquals(TenantContext.DEMO_SUBJECT_ID, job.getSubjectId()); assertTrue(job.belongsToTenant(TenantContext.DEMO_TENANT_ID)); + assertFalse(job.belongsToTenant(null)); + assertFalse(job.belongsToTenant(" \u0000 ")); } @Test From f8bafb431fbfc72a3f78f5fcc48ddc555d60363b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 21:24:40 +0900 Subject: [PATCH 5/6] test(security): reject control-corrupted conversion authority --- .../ConversionJobTenantAuthorityTest.java | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/clearfolio/viewer/model/ConversionJobTenantAuthorityTest.java b/src/test/java/com/clearfolio/viewer/model/ConversionJobTenantAuthorityTest.java index cf0a1684..cce4ab1b 100644 --- a/src/test/java/com/clearfolio/viewer/model/ConversionJobTenantAuthorityTest.java +++ b/src/test/java/com/clearfolio/viewer/model/ConversionJobTenantAuthorityTest.java @@ -3,13 +3,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.List; import java.util.UUID; import org.junit.jupiter.api.Test; /** * Proves that the tenant-aware conversion-job constructor never manufactures - * production authority when explicit tenant or subject claims are absent. + * or normalizes unsafe production authority when explicit claims are absent or + * control-corrupted. */ class ConversionJobTenantAuthorityTest { @@ -48,7 +50,28 @@ void explicitConstructorRejectsBlankTenantAuthorityAfterSanitization() { ) ); - assertEquals("tenantId must not be blank", exception.getMessage()); + assertEquals("tenantId must not contain control characters", exception.getMessage()); + } + + @Test + void explicitConstructorRejectsControlCorruptedTenantAuthority() { + for (String tenantId : List.of("tenant\u0000-a", "tenant\n-a", "tenant\u001B-a")) { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new ConversionJob( + UUID.randomUUID(), + tenantId, + "subject-a", + "report.pdf", + "application/pdf", + "hash", + 10L, + 3 + ) + ); + + assertEquals("tenantId must not contain control characters", exception.getMessage()); + } } @Test @@ -86,6 +109,27 @@ void explicitConstructorRejectsBlankSubjectAuthorityAfterSanitization() { ) ); - assertEquals("subjectId must not be blank", exception.getMessage()); + assertEquals("subjectId must not contain control characters", exception.getMessage()); + } + + @Test + void explicitConstructorRejectsControlCorruptedSubjectAuthority() { + for (String subjectId : List.of("subject\u0000-a", "subject\t-a", "subject\u007F-a")) { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> new ConversionJob( + UUID.randomUUID(), + "tenant-a", + subjectId, + "report.pdf", + "application/pdf", + "hash", + 10L, + 3 + ) + ); + + assertEquals("subjectId must not contain control characters", exception.getMessage()); + } } } From 7b05597c960778507b087011c69bbc2eda251c36 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 21:26:16 +0900 Subject: [PATCH 6/6] fix(security): reject control-corrupted conversion authority --- .../viewer/model/ConversionJob.java | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/clearfolio/viewer/model/ConversionJob.java b/src/main/java/com/clearfolio/viewer/model/ConversionJob.java index d6054c06..1a9cb7a5 100644 --- a/src/main/java/com/clearfolio/viewer/model/ConversionJob.java +++ b/src/main/java/com/clearfolio/viewer/model/ConversionJob.java @@ -85,9 +85,9 @@ public ConversionJob( * Creates a conversion job with explicit tenant and subject ownership metadata. * *

This authority-bearing constructor fails closed when tenant or subject - * claims are absent. Development callers that intentionally use demo - * authority must use one of the convenience constructors that supplies the - * demo identities explicitly.

+ * claims are absent or control-corrupted. Development callers that + * intentionally use demo authority must use one of the convenience + * constructors that supplies the demo identities explicitly.

* * @param jobId job identifier * @param tenantId tenant isolation boundary @@ -131,11 +131,24 @@ private String sanitize(String value) { } private String requireAuthority(String value, String fieldName) { - String sanitized = sanitize(value); - if (sanitized == null || sanitized.isBlank()) { + if (value == null) { throw new IllegalArgumentException(fieldName + " must not be blank"); } - return sanitized.strip(); + if (value.codePoints().anyMatch(ConversionJob::isDisallowedAuthorityCharacter)) { + throw new IllegalArgumentException(fieldName + " must not contain control characters"); + } + String normalized = value.strip(); + if (normalized.isBlank()) { + throw new IllegalArgumentException(fieldName + " must not be blank"); + } + return normalized; + } + + private static boolean isDisallowedAuthorityCharacter(int codePoint) { + int characterType = Character.getType(codePoint); + return Character.isISOControl(codePoint) + || characterType == Character.LINE_SEPARATOR + || characterType == Character.PARAGRAPH_SEPARATOR; } private String normalizeOrDefault(String value, String fallback) {