Skip to content

Security: MSKazemi/yazses

.github/SECURITY.md

Security Policy

YazSes is an offline, on-device voice-dictation tool. By design, audio and transcribed text stay on the local machine and nothing is sent to any network service by default. The opt-in learning corpus is stored encrypted (AES-256-GCM, machine-bound key). Security and privacy are core to the project, so vulnerability reports are taken seriously.

Supported versions

Security fixes are applied to the latest released version on the main branch. Please upgrade to the newest release before reporting an issue.

Version Supported
Latest release ✅
Older releases ❌ (please upgrade)

Reporting a vulnerability

Please do not open a public GitHub issue for security vulnerabilities.

Instead, report privately through either channel:

Please include:

  • a description of the issue and its potential impact,
  • steps to reproduce (proof-of-concept if available),
  • affected version(s) and platform (Linux / macOS / Windows), and
  • any suggested remediation.

What to expect

  • Acknowledgement of your report within 7 days.
  • An initial assessment and severity triage shortly after.
  • Coordinated disclosure: a fix and a public advisory once a patch is available. Reporters are credited unless they prefer to remain anonymous.

Scope

In scope: the YazSes daemon, CLI, IPC layer, injection backends, remote agent, and the encrypted learning corpus. The Android app will be in scope once it exists; until then, reports against its design (the ADRs in docs/mobile/adr/) are welcome as public issues — there is nothing to exploit yet.

Out of scope: vulnerabilities in third-party dependencies (report those upstream), and issues that require an already-compromised local account with the same privileges as the user running YazSes.

Known advisories in dependencies

"Out of scope" is not the same as "unanswered". An advisory against a dependency still shows up in anyone's supply-chain scan of this repository, and leaving it with no published reasoning is how a real finding later gets waved through by someone who has learned the alerts are noise. So each open advisory that this project does not simply upgrade away is assessed here — whether because no upstream patch exists, or because something here holds the dependency below the fix — and each assessment is pinned by a test in tests/test_dependency_advisories.py — the reasoning below cannot quietly stop being true without the suite failing.

How the list is produced

Two independent sources, both reproducible without installing anything into the project environment:

# 1. What OpenSSF Scorecard reported (read-only public API, weekly scan of main):
curl -s https://api.securityscorecards.dev/projects/github.com/MSKazemi/yazses \
  | jq '.checks[] | select(.name == "Vulnerabilities")'

# 2. A cross-check against the lock, every extra and group, all platforms:
uv export --frozen --all-extras --all-groups --no-hashes --no-emit-project \
  -o /tmp/yazses-all.txt
uvx pip-audit -r /tmp/yazses-all.txt --no-deps --disable-pip -s osv
# ...and the one hash-pinned closure that lives outside uv.lock:
uvx pip-audit -r packaging/flatpak/tools-requirements.txt --no-deps --disable-pip -s osv

# 3. The scanner Scorecard itself runs, honouring osv-scanner.toml
#    (a release binary from github.com/google/osv-scanner, checksum-verified):
osv-scanner scan source -L uv.lock -L sbom.cdx.json \
  -L requirements.txt:packaging/flatpak/tools-requirements.txt

Scorecard runs osv-scanner over every manifest in the tree, so it also reads packaging/flatpak/tools-requirements.txt (and sbom.cdx.json, which is generated from uv.lock and therefore carries the same findings). The export in step 2 covers the universal lock — every extra on every platform — which is deliberately wider than any one install.

October 2026 sweep (#116) — 21 advisories, 20 closed by upgrading

Scorecard's scan of 2026-10-20 scored Vulnerabilities 0/10 with 21 open advisories. pip-audit against the lock found the same 20, and osv-scanner 2.7.0 over the three files above reproduced all 21 — the 21st being uv in the Flatpak tools file. After the upgrades below, the same osv-scanner run reports only diskcache, which osv-scanner.toml then filters. Each was checked for whether YazSes actually reaches the vulnerable code. All but diskcache had a patched release compatible with every other pin — including the Intel-macOS onnxruntime<1.24 split, which the upgrade left untouched — so they were upgraded rather than argued about, and tests/test_dependency_advisories.py now fails if any of them falls back below its fix.

Package (was → now) Advisories Reached by YazSes?
certifi 2026.6.17 → 2026.10.19 PYSEC-2026-4320 Yes, in a default install. It is the CA bundle behind huggingface-hub/httpx, i.e. the first-run model download. An on-path attacker holding one of the compromised emSign intermediates could have presented a trusted certificate for that connection. The most relevant fix of the sweep.
Pillow 12.3.0 → 13.0.0 (floor raised to >=13.0.0) GHSA-462x-pgg6-55fp, GHSA-58r4-6rv3-gfh8, GHSA-65qm-jvmh-6pqg, GHSA-8g5g-8vmr-9782, GHSA-c6hw-5grf-ff96, GHSA-p4wg-p73j-c528, GHSA-pchr-8j38-3x4p, GHSA-q45j-58j2-45rg, GHSA-vx6w-vv9f-6gvg Installed, not reached. Pillow is a base dependency on Windows (the pystray tray) and is in the docs/dev tooling. Every advisory is in a decoder (TIFF, JPEG, GIF, PDF, EPS, morph); src/yazses/ never opens an image file — brandmark.py only draws into new in-memory images. The floor was raised anyway, because pip's only-if-needed upgrade would otherwise keep a Windows user on 12.x.
aiohttp 3.14.3 → 3.14.5 (yarl 1.24.5 → 1.25.1, required by it) GHSA-2g87-pp6c-29x5, GHSA-494j-54xm-rwp3, GHSA-5wr8-gcqf-gqf3, GHSA-6cjc-53gc-5rqm, GHSA-9xw5-w5jj-pvcg, GHSA-f9xw-jh8m-3mwq, GHSA-gx76-c99x-2c96, GHSA-pqxq-w44r-73xw Not reached. Arrives only as fsspec[http] under lightning under the opt-in diarization-pyannote extra. Nothing here imports aiohttp or runs an aiohttp server, and most of the eight are server-side.
pytorch-lightning 2.6.5 → 2.6.6 PYSEC-2026-3967 (= CVE-2026-58659) Installed, not imported. The twin distribution of the lightning checkpoint loader assessed below. pyannote.audio 4.0.7 imports lightning, never pytorch_lightning (checked in its wheel), but lightning depends on pytorch-lightning, so it was installed at the vulnerable version while the entry below described the CVE as patched.
uv 0.12.18 → 0.13.4, in packaging/flatpak/tools-requirements.txt PYSEC-2026-4321 CI only. The Flatpak generate job uses this uv to resolve the wheels Flathub builds from, over TLS to PyPI, with a vendored CA store that trusted the same compromised root. Dependabot does not read this file.
diskcache 5.6.3 PYSEC-2026-2447 Not reached; no fix exists. Assessed in its own entry below, and listed with a reason and an expiry in osv-scanner.toml, which Scorecard honours (its docs). The ignore lapses on its date so the assessment is re-made rather than inherited.

A lock upgrade protects the bundles built from the lock (.exe, .dmg) and every fresh resolve. An existing pip install keeps its old transitive packages until they are upgraded: pip install --upgrade --upgrade-strategy eager yazses picks these fixes up, as does upgrading the named packages directly (pip install --upgrade certifi). Whether uv tool upgrade re-resolves transitive packages in an existing tool environment was not checked for this entry.

diskcache ≤ 5.6.3 — unsafe pickle deserialization

Not exploitable in YazSes as shipped. No patched release exists upstream, so the Dependabot alert stays open until one does. For OSV/Scorecard it is listed in osv-scanner.toml as PYSEC-2026-2447 with this reasoning and an ignoreUntil of 2027-01-21, after which it is reported again and re-assessed.

diskcache is not a dependency of YazSes. It arrives only underneath llama-cpp-python, and two things have to be true for the advisory to bite — neither is:

  1. A default install never downloads it. llama-cpp-python appears only in the slm, notes and all extras, never in project.dependencies. Unless you opted into a local LLM feature, the package is not on your machine.
  2. Nothing here ever deserializes a cache. The vulnerability is in unpickling a diskcache file. llama_cpp reads one only when a caller installs a cache object via Llama.set_cache(...). YazSes never calls it, never constructs LlamaDiskCache, and never imports diskcache — so no cache file is ever written or read.

Exploiting it would additionally require an attacker who can already write to your cache directory, which is the local-account precondition listed as out of scope above.

If you enable a local LLM feature and configure llama-cpp's disk cache yourself, that assessment no longer covers you — that is a supported thing to want, so please open an issue rather than assuming.

lightning ≤ 2.6.5 (CVE-2026-58659) — code execution from a checkpoint

Patched at the dependency level as of 2026-09-12, and independently bounded by three things this repository controls. lightning 2.6.6, released upstream 2026-09-10, restricts the _instantiator hyperparameter to an allowlist and rejects a _class_path that does not resolve to an already-imported subclass — fixing this CVE directly. uv.lock now resolves lightning>=2.6.6 — and, since the October 2026 sweep above, pytorch-lightning>=2.6.6 too: the same loader ships in both distributions, and until then only lightning had been moved. Until this release shipped, no patched version existed at all, which is why the three bounds below were written as the entire mitigation rather than as defense-in-depth; they remain true and are kept for that reason.

This entry is deliberately worded less comfortably than the diskcache one above, because the honest answer is different. diskcache is unreachable — nothing here ever constructs the object that would unpickle. This code path is executed, by the pyannote diarization backend, and what stands between it and an exploit is a precondition rather than an absence.

The vulnerability is in lightning/pytorch/core/saving.py::_load_state. A checkpoint's hyperparameters may carry an _instantiator string; lightning imports that dotted path and calls it. It is plain text in the hparams, so weights_only=True does not stop it. pyannote.audio reaches it — Pipeline.from_pretrained → Model.from_pretrained → load_from_checkpoint.

What bounds it:

  1. No shipped artifact contains it. pyannote.audio is in the diarization-pyannote and all extras only. Verified against the lockfile rather than the declaration, because a uv.lock is a universal resolution and this project has already been wrong about exactly that (see the setuptools entry below): a default uv export --no-dev emits no lightning, no pyannote, and no torch, and neither does --extra diarization. The Docker image installs yazses[diarization], the .deb installs yazses[desktop], and the snap bundles sherpa-onnx and deliberately bundles nothing from the torch family. You get this dependency only by asking for it by name.
  2. It is not the default backend. Diarization defaults to sherpa — ONNX Runtime, no torch, no lightning. backend = "pyannote" is a choice.
  3. The checkpoint is not attacker-selectable. PIPELINE_ID and SEGMENTATION_ID in yazses/recimport/pyannote_backend.py are hardcoded module constants. There is no config key that redirects them, and nothing in YazSes loads a user-supplied .ckpt from anywhere. So the malicious checkpoint has to arrive as pyannote/speaker-diarization-3.1 or pyannote/segmentation-3.0 — which means compromising those gated repositories on Hugging Face, not handing you a file.

Fact 3 is the load-bearing one and the one most likely to be broken by a future change, so it is the one the tests pin: adding a [meeting] pyannote_model config key would be an ordinary-looking feature that silently converts a supply-chain precondition into "point it at a repo". The suite fails if the ids stop being literals, if the loader is called with anything else, or if pyannote.audio becomes a base dependency. It also pins the version floor itself, so a future re-pin or downgrade cannot silently drop back below 2.6.6 and reopen a CVE this project once could not close by any means.

Residual risk, stated plainly. If those upstream repositories are compromised, you execute their code. That is true here independently of this CVE — pyannote reads its pipeline class name out of the downloaded config.yaml and imports it — so the honest summary is that trusting the model is part of using pyannote, and this advisory does not change the trust boundary so much as make it explicit. Passing revision= to pin the model to a known commit is the obvious next hardening and is not implemented: pyannote propagates a parent revision only to $model/-style child references, and pyannote/segmentation-3.0 is a separately gated repository, so how far a pin actually reaches could not be verified without accepting the gated licence. It is written down here rather than guessed at.

If you do not use pyannote diarization — which is the default — none of this is installed on your machine.

GitPython ≤ 3.2.0 (CVE-2026-114145, GHSA-83vg-56qc-22m7) — submodule cloned into .git/hooks

Resolved — the lock now takes 3.2.1. mkdocs-rss-plugin — 1.19.0, its newest release — declares GitPython<3.2, and 3.2.1 is the only fixed version: every 3.1.x is affected. So [tool.uv] override-dependencies in pyproject.toml replaces that cap with gitpython>=3.2.1. That is safe for this plugin: 3.2.0 dropped Python 3.7 and deprecated the pure-Python GitDB backend, and both docs plugins open the existing checkout with Repo(path, search_parent_directories=True) and read history with git.log and git.tag, none of which changed. The Docs job builds the site with it. The override goes when the plugin lifts its cap.

The vulnerability is in Repo.create_submodule(name, path, url): it clones before it validates path, so a path of .git/hooks plants an executable hook. It never reached a user even before the upgrade:

  1. No user ever installs it. gitpython is reachable only from the docs dependency group. It is in no extra and not in the base dependencies, so no PyPI install, installer, package or container of YazSes contains it.
  2. The submodule API is never called. Nothing in this repository imports GitPython, and the repository has no submodules.

The tests pin the version floor, so removing the override while the plugin still caps the version fails the suite, and they pin fact 1 and the second half of fact 2.

setuptools < 83.0.0 — MANIFEST.in exclusion bypass when building an sdist

Resolved — the lock now takes 84.0.0. This entry is kept rather than deleted because the way it was wrongly assessed is the useful part.

The vulnerability is in setuptools' sdist builder — on a case-insensitive, Unicode-normalising filesystem (macOS APFS/HFS+) an NFC/NFD collision can defeat a MANIFEST.in exclusion, so a file you told the packager to leave out is included in the source distribution anyway. Two facts still stand and are still guarded by tests: YazSes is built with hatchling, not setuptools ([build-system]), and there is no MANIFEST.in in this repository for the bypass to act on.

What did not stand was the third claim. The alert had been left open because the voiceprint-resemblyzer extra pinned setuptools<81 — resemblyzer needs webrtcvad, whose first line is import pkg_resources, which setuptools removed (80.10.2 has it; 83.0.0 and 84.0.0 do not). Three files said, in three different wordings, that the pin was confined to that opt-in extra. It was not:

  • uv.lock is a universal resolution — one version per package for the entire workspace. It has no way to hold a package back "only inside an extra".
  • uv export --no-dev, asking for no extras at all, emitted setuptools==80.10.2, because core ctranslate2 requires setuptools and there was a single entry to satisfy every consumer.
  • scripts/build-macos.sh and scripts/build-windows.ps1 build the shipped .dmg and .exe from that lock, so the held-back version reached release artifacts.

The pin has therefore been removed from pyproject.toml, and the ignore rule that was suppressing the security PR has been removed from .github/dependabot.yml. The remedy for that one extra now lives where the choice is made: install it and, in that environment, pip install "setuptools<81". voiceprint/factory.py already distinguishes "installed but cannot import" from "absent" and reports the real ModuleNotFoundError, so the failure is self-describing rather than silent, and .github/workflows/heavy-extras.yml applies the same remedy weekly — which is what proves the instruction works.

The guard that should have caught this read project.dependencies and optional-dependencies. Both are declarations; the property being claimed was about the resolution. It now asserts the version uv.lock actually resolves.

There aren't any published security advisories