diff --git a/scripts/generate-model-checksums.py b/scripts/generate-model-checksums.py index 3031121..7f94d39 100755 --- a/scripts/generate-model-checksums.py +++ b/scripts/generate-model-checksums.py @@ -64,8 +64,18 @@ class Entry(NamedTuple): size: int +def _https(url: str) -> str: + """Refuse anything but https before it reaches urlopen (which also reads file:).""" + if not url.startswith("https://"): + raise SystemExit(f"refusing non-https URL: {url}") + return url + + def _fetch_json(url: str): - request = urllib.request.Request(url, headers={"User-Agent": "voculinux-checksum-generator"}) + request = urllib.request.Request( + _https(url), headers={"User-Agent": "voculinux-checksum-generator"} + ) + # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected -- https enforced by _https; fixed upstream endpoints with urllib.request.urlopen(request, timeout=TIMEOUT) as response: # noqa: S310 return json.load(response) @@ -92,8 +102,11 @@ def _download_and_hash(url: str, expected_md5: str) -> tuple[str, int]: a mismatch means the listing and the file disagree, which is worth stopping for rather than pinning either one. """ - sha256, md5, size = hashlib.sha256(), hashlib.md5(), 0 - request = urllib.request.Request(url, headers={"User-Agent": "voculinux-checksum-generator"}) + sha256, md5, size = hashlib.sha256(), hashlib.md5(usedforsecurity=False), 0 + request = urllib.request.Request( + _https(url), headers={"User-Agent": "voculinux-checksum-generator"} + ) + # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected -- https enforced by _https with urllib.request.urlopen(request, timeout=TIMEOUT) as response: # noqa: S310 for chunk in iter(lambda: response.read(1024 * 1024), b""): sha256.update(chunk) diff --git a/scripts/sync_flatpak_deps.py b/scripts/sync_flatpak_deps.py index fc0982f..417b208 100755 --- a/scripts/sync_flatpak_deps.py +++ b/scripts/sync_flatpak_deps.py @@ -122,6 +122,7 @@ def parse_export(path: Path) -> dict[str, tuple[str, set[str]]]: def pypi_artifacts(name: str, version: str) -> list[dict]: url = PYPI_JSON.format(name=name, version=version) + # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected -- fixed https://pypi.org endpoint; name and version come from uv.lock with urllib.request.urlopen(url, timeout=30) as response: # noqa: S310 return json.load(response)["urls"] diff --git a/scripts/verify_release.py b/scripts/verify_release.py index 0ef6a55..a66b6d5 100755 --- a/scripts/verify_release.py +++ b/scripts/verify_release.py @@ -188,6 +188,7 @@ def check_pypi(version: str, stored: dict[str, str]) -> list[str]: try: url = f"https://pypi.org/pypi/{PYPI_PROJECT}/{version}/json" + # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected -- fixed https://pypi.org host; version is the release being verified with urllib.request.urlopen(url, timeout=PYPI_TIMEOUT) as response: payload = json.load(response) except urllib.error.HTTPError as error: diff --git a/snap/local/check_runtime.py b/snap/local/check_runtime.py index 4c33d34..f91190e 100644 --- a/snap/local/check_runtime.py +++ b/snap/local/check_runtime.py @@ -103,6 +103,7 @@ def smoke(root: Path) -> None: site = audit(root, manifest["packages"]) sys.path.insert(0, str(site)) for name in ("numpy", "requests", "psutil", "pyaudio", "cairo", "evdev", "vocalinux"): + # nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import -- names come from the literal tuple above module = importlib.import_module(name) if not module.__file__ or not Path(module.__file__).resolve().is_relative_to( site.resolve() @@ -118,6 +119,7 @@ def smoke(root: Path) -> None: "vocalinux.speech_recognition.recognition_manager", "vocalinux.text_injection.text_injector", ): + # nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import -- names come from the literal tuple above importlib.import_module(name) import gi diff --git a/src/vocalinux/text_injection/text_injector.py b/src/vocalinux/text_injection/text_injector.py index 313be68..2de0d23 100644 --- a/src/vocalinux/text_injection/text_injector.py +++ b/src/vocalinux/text_injection/text_injector.py @@ -3002,6 +3002,7 @@ class TextInjector: type_timeout = max(5, len(chunk) * self._key_delay_seconds(key_delay) * 4) try: subprocess.run( + # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-tainted-env-args.dangerous-subprocess-use-tainted-env-args -- argv list, no shell; text follows `--` cmd, check=True, stderr=subprocess.PIPE, @@ -3447,6 +3448,7 @@ class TextInjector: try: subprocess.run( + # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-tainted-env-args.dangerous-subprocess-use-tainted-env-args -- argv list, no shell; only fixed key codes and a count cmd, check=True, stderr=subprocess.PIPE, diff --git a/src/vocalinux/ui/keyboard_backends/layout_key_map.py b/src/vocalinux/ui/keyboard_backends/layout_key_map.py index 5b9f168..1398427 100644 --- a/src/vocalinux/ui/keyboard_backends/layout_key_map.py +++ b/src/vocalinux/ui/keyboard_backends/layout_key_map.py @@ -270,6 +270,7 @@ def _kde_layout_memory_paths() -> list[str]: def _read_kde_layout_memory(path: str) -> tuple[str, list[str]]: """Return ``(SwitchMode, currentLayout tokens)`` from a layout_memory.xml.""" try: + # nosemgrep: python.lang.security.use-defused-xml-parse.use-defused-xml-parse -- local KDE layout_memory.xml from the user's own session, not remote input tree = ET.parse(path) except FileNotFoundError: return "", [] diff --git a/web/src/app/__tests__/seo-assets.test.ts b/web/src/app/__tests__/seo-assets.test.ts index e0d147d..e17a234 100644 --- a/web/src/app/__tests__/seo-assets.test.ts +++ b/web/src/app/__tests__/seo-assets.test.ts @@ -86,9 +86,11 @@ describe("SEO Assets", () => { continue; } const routePath = `${prefix}/${entry.name}`; + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal -- walks this repo's own src/app tree; no external input if (fs.existsSync(path.join(dir, entry.name, "page.tsx"))) { routes.push(`https://voculinux.com${routePath}/`); } + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal -- walks this repo's own src/app tree; no external input walk(path.join(dir, entry.name), routePath); } }; diff --git a/web/src/app/__tests__/viewport-layout.test.ts b/web/src/app/__tests__/viewport-layout.test.ts index dbadd4c..32e895c 100644 --- a/web/src/app/__tests__/viewport-layout.test.ts +++ b/web/src/app/__tests__/viewport-layout.test.ts @@ -38,6 +38,7 @@ const CHROME = async function resolveBaseUrl(): Promise { if (process.env.BASE_URL) return process.env.BASE_URL.replace(/\/$/, ""); try { + // nosemgrep: typescript.react.security.react-insecure-request.react-insecure-request -- loopback dev-server probe in a test; never a shipped request const res = await fetch("http://127.0.0.1:3456/", { signal: AbortSignal.timeout(2000) }); if (res.ok) return "http://127.0.0.1:3456"; } catch {