Skip to content

[security] fix(download): contain archive extraction paths - #289

Closed
Hinotoi-agent wants to merge 1 commit into
CloakHQ:mainfrom
Hinotoi-agent:fix/archive-extraction-containment
Closed

[security] fix(download): contain archive extraction paths#289
Hinotoi-agent wants to merge 1 commit into
CloakHQ:mainfrom
Hinotoi-agent:fix/archive-extraction-containment

Conversation

@Hinotoi-agent

Copy link
Copy Markdown

Summary

This PR hardens CloakBrowser's downloaded archive extraction boundary so archive member paths must resolve inside the intended binary cache directory before extraction.

  • Replaces string-prefix containment checks with resolved path ancestry checks.
  • Applies the same ancestry check to tar member paths, tar link targets, and zip member paths.
  • Adds regression tests for sibling-prefix traversal names such as ../out_evil/owned.txt.
  • Keeps the patch focused on archive extraction safety without changing the browser launch API.

Security issues covered

Issue Impact Severity
Archive member path containment used string-prefix checks A malicious or compromised downloaded archive could place tar members outside the intended extraction directory when the escaped path shares the destination prefix High

Before this PR

  • _extract_tar() and _extract_zip() compared paths with str(member_path).startswith(str(dest_dir.resolve())).
  • That check rejects obvious traversal such as ../../../etc/passwd, but it does not prove filesystem ancestry.
  • A sibling path such as ../out_evil/owned.txt can resolve to a path whose string still starts with the destination path prefix when the destination is named out.
  • Tar extraction could then write that member outside the intended cache directory.

After this PR

  • Archive members are accepted only when the resolved destination path is a real descendant of the resolved extraction directory.
  • Tar symlink and hardlink targets are checked against the same resolved directory boundary before being included in extraction.
  • Regression tests cover direct tar traversal, tar symlink-entry traversal, and zip traversal variants.

Why this matters

CloakBrowser downloads and extracts Chromium archives into a local cache. That cache boundary should remain intact even if an archive is malformed, malicious, or unexpectedly served from a compromised/misconfigured distribution path.

A string prefix is not a filesystem containment check. Enforcing real path ancestry prevents archive entries from escaping into adjacent paths under the same parent directory.

How this differs from related issue/PR

This overlaps the archive-hardening portion of #227, but keeps the contribution intentionally narrower:

  • Harden archive extraction and add a doctor CLI #227 bundles archive extraction hardening with Windows test adjustments and a new cloakbrowser doctor CLI.
  • This PR is a minimal security-only patch for archive path containment in cloakbrowser/download.py plus focused regression tests.
  • The fix is reviewable independently if maintainers prefer to land the archive containment boundary without the unrelated diagnostics changes.

Attack flow

attacker influences a downloaded Chromium archive
    -> archive contains a member such as ../out_evil/owned.txt
        -> string-prefix check treats /tmp/.../out_evil as inside /tmp/.../out
            -> tar.extractall writes outside the intended cache directory
                -> local file write outside the extraction root

Affected code

Issue Files
Archive extraction path containment cloakbrowser/download.py, tests/test_extract.py

Root cause

Issue: archive member path containment used string-prefix comparison.

  • Path.resolve() was called, but the containment decision was made with string startswith().
  • A path can share a textual prefix with the extraction directory without being a child of that directory.
  • Symlink and hardlink entries also need their entry path checked before extraction, not only their link target text.

CVSS assessment

Issue CVSS v3.1 Vector
Archive extraction path traversal 7.5 High CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

Rationale:

  • The attacker must influence the archive that CloakBrowser downloads or is configured to consume, so attack complexity is high and user interaction is required through install/update/use of the downloader path.
  • If reached, the primitive is a local file write outside the intended extraction root, so confidentiality, integrity, and availability impact can be high depending on the escaped path and local permissions.

Safe reproduction steps

  1. Create a temporary extraction destination named out.
  2. Create a tar archive containing a member named ../out_evil/owned.txt.
  3. Run _extract_tar(archive, dest) on the vulnerable code.
  4. Observe that extraction returns normally and writes out_evil/owned.txt next to the intended destination.

Expected vulnerable behavior

  • The vulnerable code accepts the sibling-prefix path because the resolved escaped path text starts with the extraction directory text.
  • The tar member is written outside the extraction root.
  • The new regression test asserts that this is rejected and that the outside marker file is not created.

Changes in this PR

  • Adds _is_relative_to() to perform resolved path ancestry checks with Path.relative_to().
  • Uses the helper for tar member path containment before adding members to safe_members.
  • Validates tar symlink/hardlink targets against the resolved extraction root before extraction.
  • Uses the same ancestry helper for zip member checks.
  • Adds regression tests for sibling-prefix traversal and symlink-entry traversal.

Files changed

Category Files What changed
Extraction hardening cloakbrowser/download.py Replaced string-prefix checks with resolved ancestry checks for tar and zip extraction
Regression tests tests/test_extract.py Added traversal tests for sibling-prefix member names and symlink entry paths

Maintainer impact

  • No browser launch API changes.
  • No download URL, checksum, cache naming, or platform selection changes.
  • Existing safe archive layouts continue to extract normally.
  • Suspicious link targets continue to be skipped, but the path boundary is now based on filesystem ancestry instead of string prefixes.

Fix rationale

Path.relative_to() after resolution expresses the intended boundary directly: the archive member's resolved destination must be inside the resolved extraction directory. This avoids sibling-prefix false positives while keeping the existing extraction flow and macOS bundle symlink support intact.

Type of change

  • Security fix
  • Tests
  • Documentation update
  • Refactor with no behavior change

Test plan

  • python -m pytest tests/test_extract.py -q — 14 passed
  • python -m pytest tests/test_update.py tests/test_proxy.py tests/test_build_args.py tests/test_geoip.py tests/test_config.py -q — 186 passed
  • python -m ruff check cloakbrowser/download.py tests/test_extract.py
  • python -m ruff format --check cloakbrowser/download.py tests/test_extract.py
  • python -m compileall -q cloakbrowser/download.py tests/test_extract.py
  • git diff --check
  • python -m pytest -q — attempted locally, but this environment does not have playwright installed; the run stopped with ModuleNotFoundError: No module named 'playwright' in browser/stealth tests after the extraction-focused tests had passed.

Disclosure notes

  • This PR is bounded to local archive extraction path containment in the Python downloader.
  • No production services were tested.
  • No real secrets, credentials, or production endpoints are included in the reproduction.
  • This is intentionally a narrow security-only variant of the archive-hardening work also present in Harden archive extraction and add a doctor CLI #227.

@Hinotoi-agent

Copy link
Copy Markdown
Author

Closing this in favor of #227.

I rechecked the overlap: #227 already includes the same core archive-extraction containment direction using resolved path ancestry checks, plus broader Windows/diagnostics updates. This PR is the narrower security-only variant, but keeping both open would split maintainer review on the same extraction boundary.

If maintainers prefer the focused security-only patch instead of the broader #227, I can reopen or rebase this branch.

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