Skip to content

fix: zip cross directory attack vulnerability#312

Merged
chilingling merged 8 commits into
opentiny:developfrom
msslulu:fix/unzip
May 29, 2026
Merged

fix: zip cross directory attack vulnerability#312
chilingling merged 8 commits into
opentiny:developfrom
msslulu:fix/unzip

Conversation

@msslulu
Copy link
Copy Markdown
Contributor

@msslulu msslulu commented May 15, 2026

English | 简体中文

PR

PR Checklist

Please check if your PR fulfills the following requirements:

  • The commit message follows our Commit Message Guidelines
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)
  • Built its own designer, fully self-validated

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • Other... Please describe:

Background and solution

What is the current behavior?

Issue Number: N/A

What is the new behavior?

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened ZIP extraction safety to block path traversal and ensure extracted files remain inside intended directories.
    • Safer directory/file creation and more robust null/empty input handling (e.g., duplicate-removal returns an empty list for null input).
  • Tests

    • Large expansion of automated tests covering ZIP handling (including traversal protection), data flattening, JSON parsing, file validation, Base64 encode/decode, and many edge cases for improved reliability.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a null-guard to removeDuplicates, relaxes visibility of several ZIP helper methods, hardens ZIP extraction by canonicalizing and normalizing entry targets (throwing SecurityException on escapes), and greatly expands unit tests across many utility behaviors.

Changes

Utils behavior and test suite expansion

Layer / File(s) Summary
Null-guard for removeDuplicates
base/src/main/java/com/tinyengine/it/common/utils/Utils.java
removeDuplicates now returns an empty ArrayList when input is null.
ZIP extraction hardening & helper visibility
base/src/main/java/com/tinyengine/it/common/utils/Utils.java
createTempDirectory, convertMultipartFileToFile, processZipEntries, and cleanUp made package-private; processZipEntries canonicalizes tempDir with toRealPath(), normalizes resolved entry paths, logs targets, throws SecurityException if a path escapes the safe dir, and uses Files.createDirectories() (creates parent dirs for files).
Test scaffolding and lifecycle setup
base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java
Introduces broader test imports, @TempDir temp dirs, ZIP-building helpers, Mockito initialization and spy setup, and shared ObjectMapper.
Unit tests: flat(), removeDuplicates, toHump/toLine, findMaxVersion, parseJsonFileStream
base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java
Extensive flat scenarios (nested/empty/null/list handling), removeDuplicates null/empty tests, toHump/toLine edge cases, findMaxVersion variants, and parseJsonFileStream tests including BOM stripping and invalid JSON handling.
Unit tests: unzip and processZipEntries
base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java
Comprehensive unzip coverage (single/multiple/deep/named/Chinese/empty/large/directories-only), Zip Slip protection, invalid ZIP behavior, null input, and direct processZipEntries extraction/rejection tests using temp dirs.
Unit tests: Base64, isResource/isDownload, file validation, helpers
base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java
Base64 encode/decode (standard and URL-safe), invalid-base64 and non-JSON decode failures, resource/download name detection tests, and tests for createTempDirectory and convertMultipartFileToFile.
Unit tests: cleanUp and readAllBytes
base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java
Tests for cleanUp deletion scenarios (successful, missing files, mocked failures) and readAllBytes (normal, large, empty, IOException, close verification, null input).

Sequence Diagram(s)

sequenceDiagram
  participant ZipInputStream
  participant Utils_processZipEntries
  participant FileSystem
  participant Logger
  ZipInputStream->>Utils_processZipEntries: read ZipEntry
  Utils_processZipEntries->>FileSystem: tempDir.toRealPath() -> safeDir
  Utils_processZipEntries->>Utils_processZipEntries: resolve & normalize targetPath against safeDir
  Utils_processZipEntries->>Logger: log computed targetPath
  alt targetPath within safeDir
    Utils_processZipEntries->>FileSystem: Files.createDirectories(targetPath or parent)
    Utils_processZipEntries->>FileSystem: extractFile(targetPath)
  else escape detected
    Utils_processZipEntries-->>ZipInputStream: throw SecurityException
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • chilingling

Poem

I’m a rabbit testing every zip and line,
I hop through BOMs and versions, keen and fine.
Paths made safe, duplicates tucked away,
Tests bloom bright to prove each edge today. 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: zip cross directory attack vulnerability' directly and specifically summarizes the main security improvement in the changeset, which implements Zip Slip protection and path normalization in ZIP extraction.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@base/src/main/java/com/tinyengine/it/common/utils/Utils.java`:
- Around line 242-252: The code validates targetPath against safeDir but then
creates and uses new File(tempDir, zipEntry.getName()), which can bypass the
normalization/symlink check; update the extraction/read logic in Utils (the
unzip/unpack method) to use the already-validated targetPath for all filesystem
operations: when creating directories, extracting entries, and reading contents,
call Files.createDirectories(targetPath) /
Files.createDirectories(targetPath.getParent()), pass targetPath (or
targetPath.toFile() if extractFile/readFileContent require File) into
extractFile and readFileContent instead of newFile, and remove the new
File(tempDir, ...) usage so all IO goes through the validated Path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7c56d314-e82e-4809-b1ee-8a219b9b2807

📥 Commits

Reviewing files that changed from the base of the PR and between b7bf798 and 2ef9238.

📒 Files selected for processing (1)
  • base/src/main/java/com/tinyengine/it/common/utils/Utils.java

Comment thread base/src/main/java/com/tinyengine/it/common/utils/Utils.java Outdated
Comment thread base/src/main/java/com/tinyengine/it/common/utils/Utils.java
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
base/src/main/java/com/tinyengine/it/common/utils/Utils.java (1)

245-257: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the validated targetPath for extraction and reads.

Path validation is done on targetPath, but file IO still uses new File(tempDir, zipEntry.getName()). Keep all filesystem operations on the already-validated path to avoid validation/usage drift.

Suggested patch
-            File newFile = new File(tempDir, zipEntry.getName());
+            File newFile = targetPath.toFile();

             if (zipEntry.isDirectory()) {
                 // 创建目录(同时确保父目录存在)
                 Files.createDirectories(targetPath);
                 fileInfoList.add(new FileInfo(newFile.getName(), "", true));  // 添加目录
             } else {
                 // 确保父目录存在
                 if(targetPath.getParent() != null) {
                     Files.createDirectories(targetPath.getParent());
                 }
                 extractFile(zis, newFile);  // 解压文件
                 fileInfoList.add(new FileInfo(newFile.getName(), readFileContent(newFile), false));  // 添加文件内容
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@base/src/main/java/com/tinyengine/it/common/utils/Utils.java` around lines
245 - 257, The code validates and uses the Path variable targetPath but then
performs file I/O with a different File instance newFile (constructed from
tempDir and zipEntry.getName()), which can bypass validation; change the
extraction and read operations to use the validated targetPath (and its
corresponding File via targetPath.toFile() if needed) so that
Files.createDirectories, extractFile, and readFileContent operate on the same
validated path; update places that construct FileInfo (currently using
newFile.getName() and readFileContent(newFile)) to use targetPath (or
targetPath.getFileName().toString()) and readFileContent(targetPath.toFile()) to
ensure consistent, validated filesystem access.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java`:
- Around line 715-724: The test test_encodeObjectToBase64_WithMap (and the
similar test at lines 726-735) incorrectly asserts plaintext substrings on the
Base64-encoded result; update the test to decode the Base64 output from
Utils.encodeObjectToBase64 back into bytes/string (using
Base64.getDecoder().decode or equivalent), then assert the decoded payload
contains "Alice", "20", and the expected "image/" MIME substring (or parse the
decoded JSON and assert fields), instead of asserting on the encoded string
itself.
- Around line 525-532: The test assumptions are wrong for Utils.flat (it only
flattens nested Map values, not lists/arrays); update the test methods (e.g.,
test_flat_doesNotFlattenListsOrArrays and the similar case around the other
assertions) to assert that the output map still contains a single "list" key
whose value equals the original List/array (or that output.size() == 1 and
output.get("list") equals the input list/array), instead of expecting "list[0]"
/ "list[1]" keys.
- Around line 910-916: The tests test_readAllBytes_Normal and
test_readAllBytes_BufferSizeTest mock InputStream.read to return lengths but
never write into the provided byte[] buffer, so Utils.readAllBytes receives
empty data; update the mockInputStream behavior for Utils.readAllBytes in
UtilsTest.java to use a Mockito Answer (or doAnswer) that copies the expected
bytes into the invocation’s byte[] argument before returning the number of bytes
read (for multiple-read scenarios maintain a read-offset/counter to copy the
next chunk on subsequent calls). Target the mockInputStream read(...) stubbing
in test_readAllBytes_Normal and test_readAllBytes_BufferSizeTest and ensure the
Answer writes the correct slice of expectedBytes into the provided buffer and
returns the appropriate length (then -1).
- Around line 63-70: The tests create a MockedStatic named validationUtilMock
for Utils but still stub methods with plain when(Utils.method(...)) calls, so
those stubs aren’t applied to the MockedStatic; update all stubbings that target
Utils (e.g., Utils.createTempDirectory(), Utils.convertMultipartFileToFile(...),
Utils.processZipEntries(...), and Utils.unzip() related stubs) to use
validationUtilMock.when(() -> Utils.method(...)).thenReturn(...) or
thenAnswer(...) so the mocks are registered against validationUtilMock (note
validateFileStream stubbing is already correct); keep using
validationUtilMock.close() in teardown as before.

---

Duplicate comments:
In `@base/src/main/java/com/tinyengine/it/common/utils/Utils.java`:
- Around line 245-257: The code validates and uses the Path variable targetPath
but then performs file I/O with a different File instance newFile (constructed
from tempDir and zipEntry.getName()), which can bypass validation; change the
extraction and read operations to use the validated targetPath (and its
corresponding File via targetPath.toFile() if needed) so that
Files.createDirectories, extractFile, and readFileContent operate on the same
validated path; update places that construct FileInfo (currently using
newFile.getName() and readFileContent(newFile)) to use targetPath (or
targetPath.getFileName().toString()) and readFileContent(targetPath.toFile()) to
ensure consistent, validated filesystem access.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6844684a-4df6-428f-954c-83716b873b37

📥 Commits

Reviewing files that changed from the base of the PR and between 2ef9238 and f237bf5.

📒 Files selected for processing (2)
  • base/src/main/java/com/tinyengine/it/common/utils/Utils.java
  • base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java

Comment thread base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java Outdated
Comment thread base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java
Comment thread base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java Outdated
Comment thread base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java
Comment thread base/src/main/java/com/tinyengine/it/common/utils/Utils.java Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java (1)

418-424: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Test mutates output after flat(), making assertions tautological.

Utils.flat flattens the nested input {a:{y:100, z:{x:"value"}}} to {a.y=100, a.z.x="value"} (size 2). The two output.put(...) calls at Lines 419-420 inject values into the result before the assertions, so assertEquals(3, ...), a.z.x, and a.z.y pass regardless of the real flatten logic. Note a.z.y is not even a key the implementation produces (a.y is), and a.y is never validated.

🐛 Proposed fix: assert on actual flatten output
         Map<String, Object> output = Utils.flat(input);
-        output.put("a.z.x", "value");
-        output.put("a.z.y", 100);
         assertNotNull(output);
-        assertEquals(3, output.size());
+        assertEquals(2, output.size());
+        assertEquals(100, output.get("a.y"));
         assertEquals("value", output.get("a.z.x"));
-        assertEquals(100, output.get("a.z.y"));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java` around
lines 418 - 424, The test mutates the result of Utils.flat and thus makes
assertions tautological; remove the two output.put(...) calls and instead assert
the true flattened result from Utils.flat in UtilsTest (variable output): expect
size 2, assertEquals("value", output.get("a.z.x")), and assertEquals(100,
output.get("a.y")). Keep references to Utils.flat and the local variable output
so the test validates the real flatten logic.
🧹 Nitpick comments (1)
base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java (1)

91-105: 💤 Low value

Spy/mock scaffolding in setUp() is unused.

@InjectMocks Utils utils, Mockito.spy(Utils.class), and @Mock MultipartFile mockFile are not referenced by any test (all call the static Utils.* methods directly, and mockFile is reassigned to a MockMultipartFile). This is leftover scaffolding from the earlier mockStatic approach and can be removed to reduce confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java` around
lines 91 - 105, The setUp() contains unused mock/spy scaffolding: remove the
`@InjectMocks` Utils utils declaration, the Mockito.spy(Utils.class) call, and the
`@Mock` MultipartFile mockFile field since tests call static Utils.* methods and
reassign mockFile to a MockMultipartFile; simplify setUp() to only initialize
the MockMultipartFile (mockFile = new MockMultipartFile(...)) or delete mockFile
entirely if not used, and remove any unused MockitoAnnotations.openMocks(this)
invocation to eliminate dead test setup code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java`:
- Around line 596-599: The negative contains assertions on the Base64 string
(variable base64) are brittle; instead decode the Base64 output and assert the
decoded payload contains the expected values/fields. Locate the test that builds
the base64 variable (see assertions referencing base64) and replace the
assertFalse checks with code that Base64-decodes base64, converts bytes to a
string (or parses JSON if applicable), and then assertThat the decoded
string/parsed object contains the expected fields/values (e.g., using
assertTrue/equals for specific keys) to verify proper encoding by
round-tripping.

---

Outside diff comments:
In `@base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java`:
- Around line 418-424: The test mutates the result of Utils.flat and thus makes
assertions tautological; remove the two output.put(...) calls and instead assert
the true flattened result from Utils.flat in UtilsTest (variable output): expect
size 2, assertEquals("value", output.get("a.z.x")), and assertEquals(100,
output.get("a.y")). Keep references to Utils.flat and the local variable output
so the test validates the real flatten logic.

---

Nitpick comments:
In `@base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java`:
- Around line 91-105: The setUp() contains unused mock/spy scaffolding: remove
the `@InjectMocks` Utils utils declaration, the Mockito.spy(Utils.class) call, and
the `@Mock` MultipartFile mockFile field since tests call static Utils.* methods
and reassign mockFile to a MockMultipartFile; simplify setUp() to only
initialize the MockMultipartFile (mockFile = new MockMultipartFile(...)) or
delete mockFile entirely if not used, and remove any unused
MockitoAnnotations.openMocks(this) invocation to eliminate dead test setup code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 539355f4-c57b-4432-b5c3-7d90d1c31f44

📥 Commits

Reviewing files that changed from the base of the PR and between f237bf5 and b507af3.

📒 Files selected for processing (1)
  • base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java

Comment thread base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java Outdated
Comment thread base/src/test/java/com/tinyengine/it/common/utils/UtilsTest.java Outdated
@chilingling chilingling merged commit 56c1f3a into opentiny:develop May 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants