Skip to content

Refactor unzipToTempDir with security checks and limits - #75

Closed
HGBits wants to merge 1 commit into
pass-with-high-score:mainfrom
HGBits:patch-1
Closed

Refactor unzipToTempDir with security checks and limits#75
HGBits wants to merge 1 commit into
pass-with-high-score:mainfrom
HGBits:patch-1

Conversation

@HGBits

@HGBits HGBits commented May 5, 2026

Copy link
Copy Markdown

Fixed path traversal and zip bomb crashes

I left loose limits because I don't know the exact range of the skins. Please review the proposal and correct what is necessary.

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced security validation for zip file extraction to prevent malicious or corrupted archives from causing harm. Added safeguards including entry count limits, file size restrictions, and path traversal attack prevention.

Fixed path traversal and zip bomb crashes

I left loose limits because I don't know the exact range of the skins. Please review the proposal and correct what is necessary.
@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The unzipToTempDir method in AnekoViewModel now enforces ZIP extraction security limits and validates entry paths. A new companion object defines extraction constraints, and the updated method caps total entries and extracted bytes, validates entry names against path traversal patterns, and verifies canonical paths remain within the temp directory.

Changes

ZIP Extraction Security Hardening

Layer / File(s) Summary
Constants & Limits
app/src/main/java/org/nqmgaming/aneko/presentation/AnekoViewModel.kt (lines 463–466)
MAX_ZIP_ENTRIES and MAX_ZIP_BYTES constraints defined in companion object to cap extraction scope.
Core Validation & Extraction
app/src/main/java/org/nqmgaming/aneko/presentation/AnekoViewModel.kt (lines 468–513)
unzipToTempDir replaces entry-extraction logic with per-entry counters, path traversal detection (.., leading slashes), canonical-path confinement checks to prevent zip-slip, and SecurityException on policy violations.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit hops through zips with care,
Checking paths in mountain air,
No slip-ups now, no sneaky files,
Security wrapped in safety smiles! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately and concisely describes the primary change: adding security checks and limits to the unzipToTempDir function.
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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/src/main/java/org/nqmgaming/aneko/presentation/AnekoViewModel.kt (2)

463-513: 💤 Low value

Indentation of the new companion object and unzipToTempDir is inconsistent with the rest of the class.

The new block is indented 4 spaces less than surrounding class members (compare with parseSkinMetadata/moveSkinFilesToFinalDestination). It still parses as long as the braces match, but it makes the file harder to scan and breaks the existing convention. Reflow to match the class body's 4-space member indent.

🤖 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 `@app/src/main/java/org/nqmgaming/aneko/presentation/AnekoViewModel.kt` around
lines 463 - 513, The companion object and unzipToTempDir block are mis-indented
relative to the class members; reflow the code so the private companion object
(with MAX_ZIP_ENTRIES / MAX_ZIP_BYTES) and the fun unzipToTempDir(...) are
indented the same 4-space level as other class members (e.g., parseSkinMetadata
and moveSkinFilesToFinalDestination), ensuring braces and inner lines keep their
existing relative indentation.

487-493: 💤 Low value

name.contains("..") is overly strict and redundant with the canonical-path check.

contains("..") rejects legitimate filenames such as cat..frame1.png or v1..2.png. The real defense — outFile.canonicalPath.startsWith(canonicalBase) on line 492 — already prevents zip-slip regardless of how .. appears in the name. Either drop the .. substring check or restrict it to per-component equality (split('/', '\\').any { it == ".." }). The leading / and \ checks are fine to keep as a quick reject for absolute paths.

🛠️ Proposed fix (component-wise check)
-                if (name.startsWith("/") || name.startsWith("\\")
-                        || name.contains(".."))
-                    throw SecurityException()
+                val parts = name.split('/', '\\')
+                if (name.startsWith("/") || name.startsWith("\\") || parts.any { it == ".." })
+                    throw SecurityException("Suspicious zip entry name: $name")
🤖 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 `@app/src/main/java/org/nqmgaming/aneko/presentation/AnekoViewModel.kt` around
lines 487 - 493, The check that rejects any name containing ".." is too strict;
update the validation around the variable name in the file (the block that
creates outFile and compares outFile.canonicalPath to canonicalBase in
AnekoViewModel) to either remove the name.contains("..") check entirely or
replace it with a per-path-component check such as splitting on '/' and '\' and
rejecting only if any component == ".."; keep the existing leading '/' and '\'
checks and retain the outFile.canonicalPath.startsWith(canonicalBase) security
check to prevent zip-slip.
🤖 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 `@app/src/main/java/org/nqmgaming/aneko/presentation/AnekoViewModel.kt`:
- Around line 478-502: Update the four message-less throws of SecurityException
inside the ZIP extraction loop in AnekoViewModel (the block that checks
entryCount vs MAX_ZIP_ENTRIES, the name validation using entry.name, the
zip-slip check using outFile.canonicalPath vs canonicalBase, and the byte cap
check using totalBytes vs MAX_ZIP_BYTES) to include clear descriptive messages
(e.g. "zip entry limit exceeded", "invalid/skipped entry name", "zip-slip
detected", "zip byte limit exceeded"); ensure these messages are created where
entryCount, name, outFile.canonicalPath and totalBytes are validated so the
outer importSkinFromStream catch can inspect e.message and map specific failures
to user-facing Toasts or better log entries.
- Around line 463-466: AnekoViewModel currently has a second private companion
object declaring MAX_ZIP_ENTRIES and MAX_ZIP_BYTES which causes a compile error;
move these two constants into the existing companion object that contains
PREF_KEY_THEME / PREF_KEY_ACCENT (merge MAX_ZIP_ENTRIES and MAX_ZIP_BYTES into
that companion), then delete the redundant private companion object so only one
companion object remains in the AnekoViewModel class.

---

Nitpick comments:
In `@app/src/main/java/org/nqmgaming/aneko/presentation/AnekoViewModel.kt`:
- Around line 463-513: The companion object and unzipToTempDir block are
mis-indented relative to the class members; reflow the code so the private
companion object (with MAX_ZIP_ENTRIES / MAX_ZIP_BYTES) and the fun
unzipToTempDir(...) are indented the same 4-space level as other class members
(e.g., parseSkinMetadata and moveSkinFilesToFinalDestination), ensuring braces
and inner lines keep their existing relative indentation.
- Around line 487-493: The check that rejects any name containing ".." is too
strict; update the validation around the variable name in the file (the block
that creates outFile and compares outFile.canonicalPath to canonicalBase in
AnekoViewModel) to either remove the name.contains("..") check entirely or
replace it with a per-path-component check such as splitting on '/' and '\' and
rejecting only if any component == ".."; keep the existing leading '/' and '\'
checks and retain the outFile.canonicalPath.startsWith(canonicalBase) security
check to prevent zip-slip.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8c13c23a-28ea-4228-8706-e710a2d8f994

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0181e and a5f3dc1.

📒 Files selected for processing (1)
  • app/src/main/java/org/nqmgaming/aneko/presentation/AnekoViewModel.kt

Comment on lines +463 to +466
private companion object {
const val MAX_ZIP_ENTRIES = 200
const val MAX_ZIP_BYTES = 50L * 1024 * 1024
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: duplicate companion object in the same class — won't compile.

AnekoViewModel already declares a companion object at lines 46–52 (PREF_KEY_THEME, PREF_KEY_ACCENT, …). Kotlin allows at most one companion object per class, so adding this second private companion object at line 463 produces a compile error (Only one companion object is allowed per class). Merge the new constants into the existing companion object instead.

🛠️ Proposed fix
     companion object {
         const val PREF_KEY_THEME = "theme"
         const val PREF_KEY_ACCENT = "accent_color"
         const val PREF_KEY_DYNAMIC_COLOR = "dynamic_color"
         const val PREF_KEY_FINISHED_SETUP = "finished_setup"
         const val PREF_KEY_KNOWN_SKINS = "known_skins"
+
+        private const val MAX_ZIP_ENTRIES = 200
+        private const val MAX_ZIP_BYTES = 50L * 1024 * 1024
     }

And remove lines 463–466.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private companion object {
const val MAX_ZIP_ENTRIES = 200
const val MAX_ZIP_BYTES = 50L * 1024 * 1024
}
companion object {
const val PREF_KEY_THEME = "theme"
const val PREF_KEY_ACCENT = "accent_color"
const val PREF_KEY_DYNAMIC_COLOR = "dynamic_color"
const val PREF_KEY_FINISHED_SETUP = "finished_setup"
const val PREF_KEY_KNOWN_SKINS = "known_skins"
private const val MAX_ZIP_ENTRIES = 200
private const val MAX_ZIP_BYTES = 50L * 1024 * 1024
}
🤖 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 `@app/src/main/java/org/nqmgaming/aneko/presentation/AnekoViewModel.kt` around
lines 463 - 466, AnekoViewModel currently has a second private companion object
declaring MAX_ZIP_ENTRIES and MAX_ZIP_BYTES which causes a compile error; move
these two constants into the existing companion object that contains
PREF_KEY_THEME / PREF_KEY_ACCENT (merge MAX_ZIP_ENTRIES and MAX_ZIP_BYTES into
that companion), then delete the redundant private companion object so only one
companion object remains in the AnekoViewModel class.

Comment on lines +478 to +502
if (++entryCount > MAX_ZIP_ENTRIES)
throw SecurityException()

val name = entry.name
val skip = entry.isDirectory
|| name.startsWith("__MACOSX")
|| name.endsWith(".DS_Store")

if (!skip) {
if (name.startsWith("/") || name.startsWith("\\")
|| name.contains(".."))
throw SecurityException()

val outFile = File(tempDir, name)
if (!outFile.canonicalPath.startsWith(canonicalBase))
throw SecurityException()

outFile.parentFile?.mkdirs()

BufferedOutputStream(FileOutputStream(outFile)).use { bos ->
var count: Int
while (zis.read(buffer).also { count = it } != -1) {
totalBytes += count
if (totalBytes > MAX_ZIP_BYTES)
throw SecurityException()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Throw SecurityException with a descriptive message.

All four throw SecurityException() sites are message-less, so when extraction aborts there's nothing in logs/Timber to indicate which guard fired (entry-count cap, suspicious name, zip-slip, or byte cap). The outer catch (e: Exception) in importSkinFromStream (line 403) only calls e.printStackTrace() and returns null, which silently surfaces as a generic import failure to the user. Add a reason to each throw so failures are diagnosable and so the catch site can map them to a clearer Toast.

🛠️ Proposed fix
-            if (++entryCount > MAX_ZIP_ENTRIES)
-                throw SecurityException()
+            if (++entryCount > MAX_ZIP_ENTRIES)
+                throw SecurityException("ZIP exceeds max entries ($MAX_ZIP_ENTRIES)")
@@
-                if (name.startsWith("/") || name.startsWith("\\")
-                        || name.contains(".."))
-                    throw SecurityException()
+                if (name.startsWith("/") || name.startsWith("\\")
+                        || name.contains(".."))
+                    throw SecurityException("Suspicious zip entry name: $name")
@@
-                if (!outFile.canonicalPath.startsWith(canonicalBase))
-                    throw SecurityException()
+                if (!outFile.canonicalPath.startsWith(canonicalBase))
+                    throw SecurityException("Zip-slip detected for entry: $name")
@@
-                        if (totalBytes > MAX_ZIP_BYTES)
-                            throw SecurityException()
+                        if (totalBytes > MAX_ZIP_BYTES)
+                            throw SecurityException("ZIP exceeds max uncompressed size ($MAX_ZIP_BYTES bytes)")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (++entryCount > MAX_ZIP_ENTRIES)
throw SecurityException()
val name = entry.name
val skip = entry.isDirectory
|| name.startsWith("__MACOSX")
|| name.endsWith(".DS_Store")
if (!skip) {
if (name.startsWith("/") || name.startsWith("\\")
|| name.contains(".."))
throw SecurityException()
val outFile = File(tempDir, name)
if (!outFile.canonicalPath.startsWith(canonicalBase))
throw SecurityException()
outFile.parentFile?.mkdirs()
BufferedOutputStream(FileOutputStream(outFile)).use { bos ->
var count: Int
while (zis.read(buffer).also { count = it } != -1) {
totalBytes += count
if (totalBytes > MAX_ZIP_BYTES)
throw SecurityException()
if (++entryCount > MAX_ZIP_ENTRIES)
throw SecurityException("ZIP exceeds max entries ($MAX_ZIP_ENTRIES)")
val name = entry.name
val skip = entry.isDirectory
|| name.startsWith("__MACOSX")
|| name.endsWith(".DS_Store")
if (!skip) {
if (name.startsWith("/") || name.startsWith("\\")
|| name.contains(".."))
throw SecurityException("Suspicious zip entry name: $name")
val outFile = File(tempDir, name)
if (!outFile.canonicalPath.startsWith(canonicalBase))
throw SecurityException("Zip-slip detected for entry: $name")
outFile.parentFile?.mkdirs()
BufferedOutputStream(FileOutputStream(outFile)).use { bos ->
var count: Int
while (zis.read(buffer).also { count = it } != -1) {
totalBytes += count
if (totalBytes > MAX_ZIP_BYTES)
throw SecurityException("ZIP exceeds max uncompressed size ($MAX_ZIP_BYTES bytes)")
🤖 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 `@app/src/main/java/org/nqmgaming/aneko/presentation/AnekoViewModel.kt` around
lines 478 - 502, Update the four message-less throws of SecurityException inside
the ZIP extraction loop in AnekoViewModel (the block that checks entryCount vs
MAX_ZIP_ENTRIES, the name validation using entry.name, the zip-slip check using
outFile.canonicalPath vs canonicalBase, and the byte cap check using totalBytes
vs MAX_ZIP_BYTES) to include clear descriptive messages (e.g. "zip entry limit
exceeded", "invalid/skipped entry name", "zip-slip detected", "zip byte limit
exceeded"); ensure these messages are created where entryCount, name,
outFile.canonicalPath and totalBytes are validated so the outer
importSkinFromStream catch can inspect e.message and map specific failures to
user-facing Toasts or better log entries.

@HGBits HGBits closed this by deleting the head repository May 23, 2026
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.

1 participant