Skip to content

Fix path traversal vulnerabilities#31808

Open
filips wants to merge 3 commits into
Koenkk:devfrom
filips:fix-path-traversal
Open

Fix path traversal vulnerabilities#31808
filips wants to merge 3 commits into
Koenkk:devfrom
filips:fix-path-traversal

Conversation

@filips
Copy link
Copy Markdown
Contributor

@filips filips commented Apr 28, 2026

These vulnerabilities let you access (read/write/delete) arbitrary files that the user running z2m has access to, provided you have access to MQTT directly or alternatively an unauthenticated frontend.

Each commit has more details about the issue it addresses.

@burmistrzak
Copy link
Copy Markdown
Contributor

@filips Great catch!
Have you tried breaching containment of containers on HAOS via this method?

Just yesterday, I was thinking about proposing a SECURITY.md policy... Excellent timing, I guess. 😬

cc. @Koenkk @Nerivec

@filips
Copy link
Copy Markdown
Contributor Author

filips commented Apr 29, 2026

@burmistrzak No but wouldn't be possible without a separate vulnerability in the container runtime..

Arguably the external converters/external extensions interfaces are RCE vectors by design, so fixing the path traversal alone is perhaps ignoring the elephant in the room.

Yeah a SECURITY.md policy wouldn't be a bad idea 😅

@Nerivec
Copy link
Copy Markdown
Collaborator

Nerivec commented Apr 29, 2026

Should always reject, not sometimes reject, sometimes replace, for more consistent UX. Should use a project-wide pattern per intended use (e.g. filename - extension can be validated separately). Should not ignore coverage (and if unreachable, code is not needed).
For filenames/paths, I think we can just normalize or resolve and check against expected base before doing anything with it; fixes the traversal issue without having to do double-checks that inevitably result in unreachable code.
Also, this ends up a breaking change in current form (regexes are far more limiting that just avoiding vulnerable patterns).

Although, there are plenty of other cases that would still allow bypass (e.g. null bytes, unicodes, a bunch of OS-specific stuff, also using sep alone isn't enough, etc.).
Also as mentioned, external JS is by design, allowing full access... same applies to pretty much everything in something like Z2M though (e.g. uploading a firmware overrides a device's behavior, changing a configuration can DoS the network, etc.)...
Untrusted input is realistically superseded by the fact that anything untrusted is a de-facto security issue for something like Z2M.

If a bad user has enough access to even contemplate doing any of this, there is a much bigger security issue in the setup (out-of-scope of Z2M). A path traversal at that point, would be the last of the worries, since the user would have access to the entire smart home...
In short, this is limited to "users attacking themselves"... 🤷

A software like Z2M must be secured from untrusted users at the setup level; anything untrusted below that, opens up unending security issues, since the whole point is to manage a network of devices.

@kennylevinsen
Copy link
Copy Markdown
Contributor

A software like Z2M must be secured from untrusted users at the setup level; anything untrusted below that, opens up unending security issues, since the whole point is to manage a network of device

Taking this position would require a significant public change of stance, such as:

  • Having warnings in documentation, possibly as banner in the UI that the frontend is an internal debug tool that should be left off and must never be made accessible.
  • Have the installation documentation make appropriate deployment recommendations and warnings to have the required sandboxing
  • Adding the obvious warnings about MQTT also being a way to mess up Z2M, requiring MQTT to be entirely locked down and preferably not exposed on the network.
  • Stating that access to Z2M in general means instant remote code execution, and that extreme care must be taken during deployment
  • Pushing a security advisory so that existing users who followed the existing documentation knows to turn Z2M off until they have redeployed properly

Right now a regular user has no reason to suspect that Z2M deployed as per documentation would give immediate remote code execution as your own user (assuming you followed the standard "Linux" or "Windows" installation sections) - a scenario which is a whole lot different from allowing others to mess up their Zigbee network, and is the kind of thing that triggers should trigger immediate CVE and public security advisory.

filips added 3 commits April 29, 2026 16:29
The base64 image regex used a greedy `.+` for the type capture,
letting values like `data:image/png/../../foo;base64,...` flow into
path.join() in saveBase64DeviceIcon. Anchor the regex on the data
URI structure and replace the post-hoc extension Set with a single
mime → file-extension lookup that matchBase64File consults at parse
time. The lookup, not the regex, gates which types are accepted, so
the regex only needs to identify the data URI structure.

The lookup also corrects two latent bugs in the previous shape:
the `[A-Za-z0-9]+` extension class rejected the standard
`image/svg+xml` mime while accepting non-standard `image/svg`,
and `image/jpeg` was saved as `.jpeg` rather than the conventional
`.jpg`.
The `name` field on bridge/request/{converter,extension}/{save,remove}
flowed unvalidated into path.join(basePath, name), letting MQTT clients
write JS or delete files outside the external_* directories.

Add a shared `resolveSafeChildPath(base, name)` helper that resolves
the target and asserts it is a direct child of base. Relies on Node's
path normalization to handle traversal, absolute paths, null bytes,
unicode, and OS quirks uniformly. Use it in externalJS.getFilePath,
combined with a separate `path.extname` allowlist for .js/.cjs/.mjs.

Includes regression tests for traversed, sub-directory, non-JS, ".."
and absolute-path names.
payload.hex.file_name on bridge/request/device/ota_update/{update,
schedule} flowed unvalidated into join(baseDir, fileName), letting
MQTT clients write attacker-controlled firmware bytes to arbitrary
paths. Resolve the target via the shared resolveSafeChildPath helper
and reject anything that isn't a direct child of <data>/ota. Wrap the
writeFirmwareHexToDataDir calls in try/catch so the validation throw
surfaces as an error response on the bridge topic instead of crashing
the handler.

Replace the unschedule rmSync prefix check with the same helper.
A string-based startsWith with path.sep is bypassable by storing a
traversed payload.url ("<data>/ota/../../etc/passwd") at schedule
time: the prefix matches but the syscall resolves outside the OTA
dir. resolveSafeChildPath normalizes via path.resolve and rejects
anything whose parent isn't <data>/ota.

Includes regression tests for ".." / "." / "../escape.hex" file_name
on update + schedule, and a traversal-url unschedule case.
@filips filips force-pushed the fix-path-traversal branch from c2d4db5 to ddfe782 Compare April 29, 2026 14:46
@filips
Copy link
Copy Markdown
Contributor Author

filips commented Apr 29, 2026

I have pushed fixes adressing the comments so far.

@burmistrzak
Copy link
Copy Markdown
Contributor

burmistrzak commented Apr 29, 2026

I'd agree with the overall sentiment of @kennylevinsen's comment.

Smart home is essentially (becoming) critical infrastructure at a massive scale and being able to compromise (or hold for ransom) thousand of installations is no good.

It takes just one supply-chain attack on e.g. an unrelated HA app that uses MQTT to potentially pwn an entire home.
We need to provide at least the same level of security as proprietary bridges do.

People reasonably assume that e.g. their Zigbee-based IAS is still secure when switching to an open source solution.
Secure by default isn't that sexy of a feature but highly advisable in the case of Z2M, IMHO.

@Nerivec
Copy link
Copy Markdown
Collaborator

Nerivec commented Apr 29, 2026

I think you missed the point I was trying to make.
The whole purpose of Z2M is to grant access to the underlying network and execute actions on devices, change configs, etc.. It's more akin to the software running on an Internet router; if you make it publicly available for remote access, you better secure the setup tight. Z2M, by default, has no public exposition (specific setup by the user is required for that, which is out of scope of Z2M - there are many ways to do this, all with their own docs).

There are several notes here that are out of scope, or not properly described. For example, "remote code execution" is only "remote" if you allow public access, else it is just local code execution as intended for customs (extensions/converters).
Though I guess it does demonstrate that we need a "securing" page for installation in docs, if only to avoid misconceptions and highlight key points.

I'll create a docs PR and we can continue the discussion there instead of crowding this PR with unrelated stuff.

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.

4 participants