{"record":{"id":"25d7bc899bc36a2c","repo":"Tencent/tinker","slug":"filename-contains-nul-byte-arrays-tostring-name","errorCode":null,"errorMessage":"Filename contains NUL byte: ${Arrays.toString(nameBytes)}","messagePattern":"Filename contains NUL byte: (.+?)","errorType":"exception","errorClass":"ZipException","httpStatus":null,"severity":"error","filePath":"third-party/tinker-ziputils/src/main/java/com/tencent/tinker/ziputils/ziputil/TinkerZipEntry.java","lineNumber":168,"sourceCode":"            charset = Charset.forName(\"UTF-8\");\n        }\n        compressionMethod = it.readShort() & 0xffff;\n        time = it.readShort() & 0xffff;\n        modDate = it.readShort() & 0xffff;\n        // These are 32-bit values in the file, but 64-bit fields in this object.\n        crc = ((long) it.readInt()) & 0xffffffffL;\n        compressedSize = ((long) it.readInt()) & 0xffffffffL;\n        size = ((long) it.readInt()) & 0xffffffffL;\n        int nameLength = it.readShort() & 0xffff;\n        int extraLength = it.readShort() & 0xffff;\n        int commentByteCount = it.readShort() & 0xffff;\n        // This is a 32-bit value in the file, but a 64-bit field in this object.\n        it.seek(42);\n        localHeaderRelOffset = ((long) it.readInt()) & 0xffffffffL;\n        byte[] nameBytes = new byte[nameLength];\n        Streams.readFully(cdStream, nameBytes, 0, nameBytes.length);\n        if (containsNulByte(nameBytes)) {\n            throw new ZipException(\"Filename contains NUL byte: \" + Arrays.toString(nameBytes));\n        }\n        name = new String(nameBytes, 0, nameBytes.length, charset);\n        if (extraLength > 0) {\n            extra = new byte[extraLength];\n            Streams.readFully(cdStream, extra, 0, extraLength);\n        }\n        if (commentByteCount > 0) {\n            byte[] commentBytes = new byte[commentByteCount];\n            Streams.readFully(cdStream, commentBytes, 0, commentByteCount);\n            comment = new String(commentBytes, 0, commentBytes.length, charset);\n        }\n        /*if (isZip64) {\n            Zip64.parseZip64ExtendedInfo(this, true *//* from central directory *//*);\n        }*/\n    }\n\n    private static boolean containsNulByte(byte[] bytes) {\n        for (byte b : bytes) {","sourceCodeStart":150,"sourceCodeEnd":186,"githubUrl":"https://github.com/Tencent/tinker/blob/1b7ea02c239840f563ea64fb5bd286eb98d4011e/third-party/tinker-ziputils/src/main/java/com/tencent/tinker/ziputils/ziputil/TinkerZipEntry.java#L150-L186","documentation":"After reading an entry's name bytes from the central directory, TinkerZipEntry scans them for a NUL (0x00) byte and rejects the archive if one is present, because zip names are NUL-terminated C-style strings and an embedded NUL would truncate/ambiguate the name downstream. This is both a corruption detector and a hardening measure against zip-slip/name-confusion attacks.","triggerScenarios":"Constructing TinkerZipEntry from a central directory stream where the declared nameLength bytes include a 0x00 — caused by a corrupt central directory (wrong nameLength field), truncated downloads, or a deliberately crafted malicious archive.","commonSituations":"Archives corrupted in transit or by buggy repackaging tools that mis-write the name length field; hostile zips crafted to smuggle paths; processing untrusted uploads where malformed entries must be rejected rather than crash or misroute.","solutions":["Verify the archive's integrity before parsing (unzip -t / java.util.zip round-trip) and re-fetch or regenerate it if the central directory is damaged.","For untrusted input, validate entries with a strict pre-pass (name length, allowed characters, no NUL) and quarantine archives that fail.","If you produce archives programmatically, ensure name fields are written with the exact UTF-8 byte length and contain no NUL characters."],"exampleFix":"// before: parsing an untrusted archive directly\nTinkerZipFile zf = new TinkerZipFile(untrustedFile); // ZipException: Filename contains NUL byte\n\n// after: pre-validate with the platform reader, then hand off only clean archives\ntry (java.util.zip.ZipFile probe = new java.util.zip.ZipFile(untrustedFile)) {\n    java.util.Enumeration<? extends java.util.zip.ZipEntry> es = probe.entries();\n    while (es.hasMoreElements()) { /* name sanity checks here */ }\n} catch (java.io.IOException e) {\n    throw new IllegalArgumentException(\"rejected malformed archive\", e);\n}\nTinkerZipFile zf = new TinkerZipFile(untrustedFile);","handlingStrategy":"validation","validationCode":"// Reject archives with NUL bytes or unsafe characters in entry names before processing\nboolean namesAreClean(java.io.File f) throws java.io.IOException {\n    try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(f)) {\n        java.util.Enumeration<? extends java.util.zip.ZipEntry> es = zf.entries();\n        while (es.hasMoreElements()) {\n            String n = es.nextElement().getName();\n            if (n.indexOf('\\u0000') >= 0 || n.startsWith(\"/\") || n.contains(\"..\")) return false;\n        }\n        return true;\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    TinkerZipFile zf = new TinkerZipFile(file);\n    zf.close();\n} catch (java.util.zip.ZipException e) {\n    if (e.getMessage() != null && e.getMessage().startsWith(\"Filename contains NUL byte\")) {\n        // malformed or hostile archive: quarantine, never retry\n        quarantine(file);\n        return;\n    }\n    throw e;\n}","preventionTips":["Run untrusted archives through a strict validation pass (magic, per-entry name rules, no NUL) before parsing.","Verify integrity (unzip -t or CRC round-trip) after any transfer.","When writing archives, set name lengths to the exact UTF-8 byte count and forbid NUL in generated names."],"tags":["zip","central-directory","malformed-archive","security","tinker"],"backgroundTag":null,"analyzedSha":"1b7ea02c239840f563ea64fb5bd286eb98d4011e","analyzedAt":"2026-08-14T15:16:52.110Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}