Skip to content

Reject unsafe filesystem paths from the HACS manifest#5380

Open
frenck wants to merge 2 commits into
mainfrom
frenck/harden-release-zip-extraction
Open

Reject unsafe filesystem paths from the HACS manifest#5380
frenck wants to merge 2 commits into
mainfrom
frenck/harden-release-zip-extraction

Conversation

@frenck

@frenck frenck commented Jul 12, 2026

Copy link
Copy Markdown
Member

Proposed change

The filename and persistent_directory values from hacs.json end up in filesystem paths ({temp_dir}/{filename} in both zip download paths, and {content.path.local}/{persistent_directory} during install). They were only validated as str, so a hostile manifest could point them outside the intended directory with values like ../../custom_components/evil and place files outside its category lane.

This adds a shared is_safe_relative_path() helper that rejects absolute paths and any .. segment (both slash styles), and applies it in two places:

  • HACS_MANIFEST_JSON_SCHEMA: used by the HACS action, so default repository submissions with such values are rejected loudly at publish time.
  • HacsManifest.from_dict: the single construction point for all runtime manifests (fetched hacs.json, custom repositories, storage restore). Unsafe values are dropped with a warning instead of raising, so a bad stored manifest can never break the startup restore.

Nested relative paths like sub/dir remain allowed, existing legitimate repositories are unaffected.

For completeness: the zip extraction paths themselves were checked and are not vulnerable to zip-slip, Python's zipfile.extractall sanitizes .. components and absolute member names. This change is about the manifest values, which bypass that sanitization because they are used to build paths directly.

Type of change

  • Enhancement (hardening, non-breaking)

Checklist

  • The code change is tested and works locally.
  • Local tests pass.
  • There is no commented out code in this PR.

Copilot AI review requested due to automatic review settings July 12, 2026 18:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens HACS manifest handling by preventing repository-provided manifest fields (filename, persistent_directory) from being used to form unsafe filesystem paths that could escape intended directories.

Changes:

  • Introduces is_safe_relative_path() and applies it to manifest path-like fields.
  • Enforces safe-relative-path validation in the publish-time schema (HACS_MANIFEST_JSON_SCHEMA).
  • Sanitizes unsafe values at runtime in HacsManifest.from_dict() (dropping them with a warning) and adds/extends tests covering the behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
custom_components/hacs/utils/path.py Adds the shared safe-relative-path helper used to gate manifest-derived paths.
custom_components/hacs/utils/validate.py Updates the HACS manifest JSON schema to validate filename/persistent_directory as safe relative paths.
custom_components/hacs/repositories/base.py Applies runtime sanitization of filename/persistent_directory when constructing HacsManifest from a dict.
tests/utils/test_validate.py Adds schema-level tests ensuring unsafe paths and non-strings are rejected.
tests/utils/test_path.py Adds unit tests for is_safe_relative_path() behavior.
tests/repositories/test_hacs_manifest.py Adds runtime tests verifying unsafe values are ignored (and safe ones retained) during from_dict().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread custom_components/hacs/repositories/base.py
Comment thread custom_components/hacs/utils/path.py
Comment thread tests/utils/test_path.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants