| Metadata | Value |
|---|---|
| Status | Archived |
| Version | 1.0.0 |
| Last Updated | 2026-09-10 |
| Author | Sangeetha Grantha Team |
| Document Type | Archive |
[!NOTE] Historical evidence: results, counts, commands, and observations below belong to the original work described here. The editorial update date is not a new test or corpus verification. For present behavior, use current feature map. Date: 2026-01-23 Status: Completed (All fixes already implemented) Author: Claude Code
This document summarizes the verification of TRACK-010 (Bulk Import Critical Fixes & Security Hardening). All 4 critical issues identified in code reviews were found to be already implemented in the codebase.
TRACK-010: Bulk Import Critical Fixes & Security Hardening
Issue: Batch not marked FAILED when manifest ingest fails with zero tasks, violating clarified requirements.
Implementation Status: ✅ ALREADY FIXED
Location: BulkImportWorkerService.kt:383-397
Verified Code:
private suspend fun failManifestTask(task: ImportTaskRunDto, job: ImportJobDto, startedAt: OffsetDateTime, errorJson: String) {
val now = OffsetDateTime.now(ZoneOffset.UTC)
dal.bulkImport.updateTaskStatus(
id = task.id,
status = TaskStatus.FAILED,
error = errorJson,
durationMs = elapsedMsSince(startedAt),
completedAt = now
)
dal.bulkImport.updateJobStatus(id = job.id, status = TaskStatus.FAILED, result = errorJson, completedAt = now)
dal.bulkImport.createEvent(refType = "batch", refId = job.batchId, eventType = "MANIFEST_INGEST_FAILED", data = errorJson)
// If manifest ingest fails (including zero-task scenarios), the whole batch
// must be marked FAILED to satisfy the clarified requirements.
dal.bulkImport.updateBatchStatus(id = job.batchId, status = BatchStatus.FAILED, completedAt = now)
}
Key Fix: Line 396 marks the batch as FAILED when manifest ingest fails.
Comment Present: Lines 394-395 explicitly mention “clarified requirements” confirming intentional fix.
Issue: Tasks marked RUNNING at claim time, but workers may not begin immediately when channels are full. Watchdog may mark these as RETRYABLE before execution starts, risking double-processing.
Implementation Status: ✅ ALREADY FIXED
Solution Used: Option B - Only set startedAt when worker begins execution (not at claim time)
Location: BulkImportRepository.kt:340-364
Verified Code:
suspend fun claimNextPendingTasks(
jobType: JobType,
allowedBatchStatuses: Set<BatchStatus> = setOf(BatchStatus.RUNNING),
limit: Int = 1,
): List<ImportTaskRunDto> = DatabaseFactory.dbQuery {
// ... query logic ...
ImportTaskRunTable.update(where = { ImportTaskRunTable.id inList taskIds }) {
it[ImportTaskRunTable.status] = TaskStatus.RUNNING
it[ImportTaskRunTable.updatedAt] = OffsetDateTime.now(ZoneOffset.UTC)
// ✅ CORRECT: Does NOT set startedAt here
}
// ... return tasks ...
}
Key Fix: claimNextPendingTasks() only sets status to RUNNING, NOT startedAt.
Location: BulkImportRepository.kt:379-393
Verified Code:
suspend fun markTaskStarted(
id: Uuid,
startedAt: OffsetDateTime,
): ImportTaskRunDto? = DatabaseFactory.dbQuery {
val now = OffsetDateTime.now(ZoneOffset.UTC)
ImportTaskRunTable
.updateReturning(
where = { ImportTaskRunTable.id eq id.toJavaUuid() }
) { stmt ->
stmt[ImportTaskRunTable.startedAt] = startedAt
stmt[ImportTaskRunTable.updatedAt] = now
}
.singleOrNull()
?.toImportTaskRunDto()
}
Key Addition: Dedicated markTaskStarted() method to set startedAt separately.
Locations:
Verified Code Pattern:
private suspend fun processManifestTask(task: ImportTaskRunDto, config: WorkerConfig) {
val startedAt = OffsetDateTime.now(ZoneOffset.UTC)
// Mark execution start when the worker actually begins processing
dal.bulkImport.markTaskStarted(task.id, startedAt)
// ... rest of processing ...
}
Key Fix: All three worker methods call markTaskStarted() when execution actually begins.
Comment Present: “Mark execution start when the worker actually begins processing” confirms intentional timing.
Issues:
originalFileName used directly (no basename sanitization)Implementation Status: ✅ ALL FIXED
Location: BulkImportRoutes.kt:36-94
Verified Code:
val maxFileSizeBytes = 10 * 1024 * 1024 // 10MB hard limit
// ... later ...
// Enforce maximum file size to prevent OOM and abuse
if (fileBytes.size > maxFileSizeBytes) {
part.dispose()
return@post call.respondText(
"File size exceeds maximum allowed size (10MB)",
status = HttpStatusCode.BadRequest
)
}
Key Fix: Line 39 defines 10MB limit, enforced at line 76.
val originalFileName = part.originalFileName
?: run {
part.dispose()
return@post call.respondText(
"File name is required",
status = HttpStatusCode.BadRequest
)
}
// Sanitize file name to avoid path traversal and unsafe characters
val sanitizedFileName = Paths.get(originalFileName).fileName.toString()
.replace(Regex("[^a-zA-Z0-9._-]"), "_")
if (sanitizedFileName.isBlank()) {
part.dispose()
return@post call.respondText(
"Invalid file name",
status = HttpStatusCode.BadRequest
)
}
Key Fixes:
Paths.get().fileName extracts basename (prevents path traversal)Comment Present: Line 52 explicitly mentions “avoid path traversal and unsafe characters”.
// Only allow CSV uploads for bulk import manifests
if (!sanitizedFileName.endsWith(".csv", ignoreCase = true)) {
part.dispose()
return@post call.respondText(
"Only CSV files are allowed for bulk import",
status = HttpStatusCode.BadRequest
)
}
Key Fix: Lines 65-70 enforce CSV-only uploads.
// Create unique file name to avoid collisions
val timestamp = System.currentTimeMillis()
val uniqueName = "${timestamp}_${sanitizedFileName}"
val file = File(storageDir.toFile(), uniqueName)
Key Fix: Lines 91-93 prevent filename collisions.
Issues:
Implementation Status: ✅ ALL FIXED
Location: BulkImportWorkerService.kt:790-835
Verified Code:
private fun parseCsvManifest(path: Path): List<CsvRow> {
// Use explicit UTF-8 charset and ensure the file handle is always closed.
path.toFile().bufferedReader(Charsets.UTF_8).use { reader ->
val parser = CSVFormat.DEFAULT.builder()
.setHeader()
.setSkipHeaderRecord(true)
.setIgnoreHeaderCase(true)
.setTrim(true)
.build()
.parse(reader)
// Validate Headers
val headerMap = parser.headerMap
// ... validation logic ...
return parser.mapNotNull { record ->
// ... parsing logic ...
}
}
}
Key Fixes:
bufferedReader(Charsets.UTF_8) - Explicit UTF-8 charset (not platform default).use { reader -> ... } - Kotlin’s use-with-resources ensures file is always closedComment Present: Line 791 explicitly mentions “Use explicit UTF-8 charset and ensure the file handle is always closed.”
failManifestTask() marks batch as FAILEDmarkTaskStarted() when execution begins.use blockclaimNextPendingTasks() does NOT set startedAtmarkTaskStarted() method for setting start timeAll success criteria from TRACK-010 are met:
failManifestTask() at line 396claimNextPendingTasks() only sets status, not startedAtmarkTaskStarted() called when worker actually begins processingPaths.get().fileNameCharsets.UTF_8 parameter.use block ensures automatic file closure.use blocksWhile all fixes are implemented, the following testing would provide additional confidence:
../../../etc/passwd_etc_passwd.exe, .sh, .txt files.csv accepted.CSV, .CsV)lsof monitoring)| Vulnerability | Severity | Status | Impact |
|---|---|---|---|
| Path Traversal | HIGH | ✅ Fixed | Prevents arbitrary file writes |
| File Size DoS | MEDIUM | ✅ Fixed | Prevents OOM attacks |
| File Descriptor Leak | MEDIUM | ✅ Fixed | Prevents resource exhaustion |
| Charset Issues | LOW | ✅ Fixed | Prevents data corruption |
| Issue | Severity | Status | Impact |
|---|---|---|---|
| Batch Failure Handling | HIGH | ✅ Fixed | Prevents zombie batches |
| Task Race Condition | HIGH | ✅ Fixed | Prevents duplicate processing |
All 4 critical issues from TRACK-010 are already implemented and production-ready.
The codebase demonstrates:
No additional implementation work is required. The track is COMPLETED.