Move SSD zip entry validation to typed Kotlin
ober
67e048fd649b38579a7d12934036bac7408f0850
--- a/templates/ssd-review.ss +++ b/templates/ssd-review.ss @@ -1273,7 +1273,7 @@ (typed-kotlin-file "com/sfb/ssdreview/PathSafety.kt" (typed-library (com sfb ssdreview) - (export safePathComponent safeLeaf validSourceKey validatedSourceKey) + (export safePathComponent safeLeaf safeZipEntryName validSourceKey validatedSourceKey) (def (safePathComponent (name : String)) : Bool (let ((n (string-length name))) (and (and (>= n 1) (<= n 128)) @@ -1284,6 +1284,17 @@ (and (safePathComponent name) (or (string-ends-with? name ".json") (string-ends-with? name ".jsonl")))) + (def (safeZipEntryName (name : String)) : Bool + (let ((n (string-length name))) + (and (and (>= n 1) (<= n 240)) + (and (not (string-starts-with? name "/")) + (and (not (string-contains? name "\\")) + (or (string-matches-regex? + name + "^(ground_truth|learned|events|reviews)/$") + (string-matches-regex? + name + "^(ground_truth|learned|events|reviews)/[A-Za-z0-9][A-Za-z0-9._-]*\\.(json|jsonl)$"))))))) (def (validSourceKey (key : String)) : Bool (let ((n (string-length key))) (and (and (>= n 1) (<= n 96)) @@ -4942,7 +4953,7 @@ " throw IllegalStateException(\"ZIP export quota exceeded\")" " }" " val relative = root.toPath().relativize(file.toPath()).toString()" - " require(safeEntry(relative)) { \"Unsafe local export path\" }" + " require(safeZipEntryName(relative)) { \"Unsafe local export path\" }" " zip.putNextEntry(ZipEntry(relative))" " file.inputStream().use { input ->" " LimitedInputStream(input, MAX_ZIP_ENTRY_BYTES).use { it.copyTo(zip, 32 * 1024) }" @@ -4966,7 +4977,7 @@ " budget.entries += 1" " if (budget.entries > MAX_ZIP_ENTRIES) throw IllegalStateException(\"ZIP entry-count limit exceeded: ${budget.entries}/$MAX_ZIP_ENTRIES\")" " val name = entry.name" - " if (name.length > MAX_ZIP_PATH_CHARS || !safeEntry(name)) {" + " if (!safeZipEntryName(name)) {" " throw IllegalStateException(\"Unsafe ZIP entry\")" " }" " if (!entry.isDirectory) {" @@ -5365,18 +5376,8 @@ " }" " }" "" - " private fun safeEntry(name: String): Boolean {" - " if (name.length !in 1..MAX_ZIP_PATH_CHARS || name.startsWith('/') || name.contains('\\\\')) return false" - " val directory = name.removeSuffix(\"/\")" - " if (name.endsWith(\"/\")) return directory in setOf(\"ground_truth\", \"learned\", \"events\", \"reviews\")" - " val parts = name.split('/')" - " return parts.size == 2 &&" - " parts[0] in setOf(\"ground_truth\", \"learned\", \"events\", \"reviews\") &&" - " safeLeaf(parts[1])" - " }" - "" " private fun containedImportPath(name: String): File {" - " require(safeEntry(name) && !name.endsWith('/')) { \"Unsafe import path\" }" + " require(safeZipEntryName(name) && !name.endsWith('/')) { \"Unsafe import path\" }" " val parts = name.split('/')" " val directory = when (parts[0]) {" " \"ground_truth\" -> groundTruthDir"