security(frontend): pin js-yaml to 5.4.1+

Raise the js-yaml override from ^5.2.3 to ^5.4.1. Versions up to 5.4.0
    do not count empty mappings towards maxTotalMergeKeys, so a small YAML
    file can keep the CPU busy for a long time. js-yaml is a dev-only
    transitive dependency of eslint and only parses our own ESLint configs.
This commit is contained in:
maziggy
2026-09-30 16:25:05 +02:00
5 changed files with 138 additions and 4 deletions
+2
View File
@@ -103,3 +103,5 @@ test_pipeline_run_1.3mf
.coverage
.coverage.*
htmlcov/
feature_votes.sh
+1
View File
@@ -221,6 +221,7 @@ All notable changes to Bambuddy will be documented in this file.
- **Bumped the build and lint toolchain for three development-dependency advisories in `browserslist` and `@humanfs/node` (GHSA-73wf-gq98-2v4g, GHSA-c83g-rgw3-j3cx, GHSA-p498-v437-472g)** — `browserslist` moves 4.28.1 → 4.28.8 for two high-severity issues: `normalizeStats()` walks an untrusted `browserslist-stats.json` with an unguarded `for...in` and uses the keys for plain bracket access and assignment, so a `__proto__` or `constructor` key either crashes the build or writes to the prototype (CVE-2026-73088), and the query-result cache has no eviction at all, so a long-lived process fed distinct queries grows without bound (CVE-2026-73089). `@humanfs/node` moves 0.16.7 → 0.16.8 for a medium-severity path-traversal issue where `copyAll()` ignores symlink state and `fs.copyFile()` dereferences the link, copying data from outside the source tree. **No running Bambuddy install was exposed, and neither issue was reachable even at build time.** Both packages are development-only — they are absent from the shipped image, and `npm audit --omit=dev`, which is what CI gates on, reported zero findings before and after. `browserslist` is never called by our own code; it arrives under `autoprefixer` and `@babel/helper-compilation-targets`, there is no `browserslist-stats.json` anywhere in the repository or up the directory tree, no `browserslist` key in `package.json` and no `.browserslistrc`, and nothing passes `--stats` or `opts.stats`, so the untrusted input the first advisory needs has no way in; the unbounded cache needs a long-lived process taking attacker-chosen queries, where `vite build` is one-shot with a fixed query. `@humanfs/node` arrives under `eslint`, which calls only `isDirectory` and `walk` and never copies anything, so the copy path the advisory describes is never entered. Lockfile-only — every existing range already admitted the patched versions, so `frontend/package.json` is untouched. The bump carries `caniuse-lite` 1.0.30001769 → 1.0.30001810, `baseline-browser-mapping` 2.9.19 → 2.11.20, `electron-to-chromium` 1.5.286 → 1.5.420, `node-releases` 2.0.27 → 2.0.54 and `update-browserslist-db` 1.2.3 → 1.3.2, all of which feed autoprefixer's target data — the rebuilt bundle is byte-identical, same content hashes, so `static/` does not change. Verified with a clean build against the Safari 16.0 baseline check, eslint, 3514 frontend tests across 256 files, i18n parity in all 13 locales, and `npm audit` reporting zero findings with and without dev dependencies.
- **Bumped `fflate` to 0.8.3 for a denial-of-service advisory reachable through three's compressed-format loaders (GHSA-px8p-9vwx-vf98, #3034)** — `unzipSync()`g `0x0001`: `z64e()` then reads past the end of the buffer, the `undefined` that comes back coerces to 0, and the loop condition can never go false, so the tab spins at 100% CPU until it is killed (CVE-2026-45820, medium, CWE-400). **No running Bambuddy install was exposed.** Unlike the other two entries here this one is classified runtime rather than development scope, so it is worth saying exactly why it cannot fire: `fflate` reaches the tree only as a dependency of `@types/three`, which is a types-only package whose imports TypeScript erases at compile time, so it never becomes a runtime import at all. The ten three.js addons that genuinely call it — `3MFLoader`, `FBXLoader`, `KMZLoader`, `AMFLoader`, `EXRLoader`, `USDLoader`, `VTKLoader`, `NRRDLoader`, `USDZExporter` and `EXRExporter` — are imported nowhere in the frontend or the backend; the four this project does use, `OrbitControls`, `BufferGeometryUtils`, `STLLoader` and `RoomEnvironment`, referencenone of it. `3MFLoader` is the one worth checking twice given what Bambuddy spends its time reading, and it is genuinely absent: 3MF files are parsed on the backend, not in the model viewer. The shipped bundle confirms it, carrying no fflate signature whatsoever — `unzipSync`, `z64e`, `invalid zip data`, `no stream handler` and `extra field too long` are all absent, and the one inflate error string that does appear belongs to pako, which the surrounding `e.msg=` / `n.mode=30` zlib-port idiom identifies unambiguously. Lockfile-only: three lines, no `frontend/package.json` change, nothing added or removed, and no transitive churn. The reasona package that never ships shows up in runtime scope at all is that `@types/three` sits in `dependencies` rather than `devDependencies`, which is left alone hererather than moved in a security bump.
- **Bumped Vitest to 4.1.11 for a path-traversal advisory in `@vitest/mocker` (GHSA-82fw-gwwq-j7x9)** — the mocker registers a redirect mock's target path without checking it against Vite's file-serving allowlist, and the plugin's `load` hook then hands back `readFile(mock.redirect)` as the module source. Registration derives that path as `join(server.config.root, new URL(event.redirect).pathname)`, which does not confine anything: a non-special scheme leaves `..` segments in `pathname`, so the join resolves outside the project root, and even a path that stays inside it is read without the `server.fs.deny` check the dev server would otherwise apply to an in-root `.env`. Medium severity, CVSS 3.1 5.9, CWE-22; fixed in 4.1.11 and 5.0.0, with 2.1.x and 3.x unmaintained. **No running Bambuddy install was exposed, and the issue is not reachable in this project's test runs either.** The unauthenticated variant is specifically the public `mockerPlugin` and standalone `interceptorPlugin` exports, which attach the handler to Vite's HMR WebSocket — a socket with no token, Origin or same-origin check — and those exports exist for third-party dev servers embedding the mocker; nothing under `frontend/src/` imports either one. Vitest's own browser mode registers mocks over a token-authenticated RPC instead, and it is not installed here at all: `@vitest/browser` appears in the lockfile only as an unmet optional peer, and `vitest.config.ts` runs a plain jsdom environment, so no dev server is listening during a test run in the first place. Both packages are development-only — absent from the shipped image, and `npm audit --omit=dev`, which is what CI gates on, reported zero findings before and after. Fifteen lockfile entries move, every one of them dev-scoped, with nothing added or removed: the eight `@vitest/*` packages and `vitest` itself go 4.1.8 → 4.1.11, carrying `es-module-lexer` 2.1.0 → 2.3.2, `expect-type` 1.3.0 → 1.4.0, `obug` 2.1.1 → 2.2.1, `std-env` 4.1.0 → 4.2.0, `tinyexec` 1.2.4 → 1.3.1 and `tinyrainbow` 3.1.0 → 3.1.1. Unlike the other entries here this one does touch `frontend/package.json`: the existing `^4.1.8` range already admitted the patched version, so the lock alone would have pinned it, but the declared floor is raised to `^4.1.11` so that a regenerated lockfile cannot resolve back below the fix. Nothing in `src/` changed, so the rebuilt bundle is byte-identical and `static/` does not move. Verified with the full frontend suite running on 4.1.11 (3562 tests across 259 files), eslint, the production build and its Safari 16.0 baseline check, i18n parity in all 13 locales, and `npm audit` reporting zero vulnerabilities with and without dev dependencies.
- **Bumped `js-yaml` to 5.4.2 for a merge-key denial-of-service advisory** — In versions up to 5.4.0, the `maxTotalMergeKeys` limit does not count empty mappings. A small YAML document that merges a long list of `{}` many times (`<<: *arr`) can keep the CPU busy for seconds per few hundred kilobytes without ever reaching the limit. It only applies when merge keys are enabled, which means the YAML 1.1 schema. The fix counts each merged mapping towards the budget. **No running Bambuddy install was exposed, and the issue was not reachable at build time either.** `js-yaml` is a development-only transitive dependency, pulled in by `eslint` through `@eslint/eslintrc`, and it is not in the shipped image. `@eslint/eslintrc` only parses legacy `.eslintrc.yaml`/`.eslintrc.yml` files, and this repository has none. Linting uses the flat `frontend/eslint.config.js`, so no YAML is loaded at all. The existing `overrides` pin in `frontend/package.json` moves from `^5.2.3` to `^5.4.1`, so a regenerated lockfile cannot resolve back below the fix. The lockfile changes in one entry only, with nothing added or removed, and the shipped bundle does not change. Verified with eslint, typecheck, the production build, and the full frontend suite (4022 tests across 297 files). `npm audit` reports no `js-yaml` findings.
## [1.2.5.4] - 2026-08-29
+131
View File
@@ -0,0 +1,131 @@
#!/bin/bash
# Rank open enhancement issues by community thumbs-up votes.
#
# A vote is a 👍 on the issue itself or on the "gauge community interest"
# comment. Each person counts once, and the issue author and the maintainer
# are left out. Read-only: it only queries the GitHub API through gh.
#
# Usage: ./feature_votes.sh --help
set -euo pipefail
show_help() {
cat <<'EOF'
Usage: ./feature_votes.sh [options] [limit]
Rank open enhancement issues (without the contrib label) by thumbs-up votes.
Arguments:
limit Show only the top N issues (default: all)
Options:
--md Print a Markdown table with issue links
-h, --help Show this help message
Columns:
votes Different people who voted on the issue or the poll comment,
without the issue author and the maintainer (ranked by this)
on issue Thumbs-up on the issue itself
on poll Thumbs-up on the "gauge community interest" comment
("-" when the issue has no such comment)
Examples:
./feature_votes.sh All issues
./feature_votes.sh 20 Top 20
./feature_votes.sh --md 20 Top 20 as Markdown
EOF
}
FORMAT=text
LIMIT=0
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) show_help; exit 0 ;;
--md) FORMAT=md ;;
*[!0-9]*|"")
echo "Unknown option: $1" >&2
echo "Run ./feature_votes.sh --help for usage." >&2
exit 1
;;
*) LIMIT="$1" ;;
esac
shift
done
command -v gh >/dev/null || { echo "gh (GitHub CLI) is required" >&2; exit 1; }
gh auth status >/dev/null 2>&1 || { echo "gh is not logged in; run: gh auth login" >&2; exit 1; }
FORMAT="$FORMAT" LIMIT="$LIMIT" python3 - <<'PY'
import json
import os
import subprocess
REPO = "maziggy/bambuddy"
MAINTAINER = "maziggy"
SEARCH = f"repo:{REPO} is:issue state:open label:enhancement -label:contrib"
POLL_MARKER = "I'd like to gauge community interest"
QUERY = """
query($q: String!, $after: String) {
search(query: $q, type: ISSUE, first: 30, after: $after) {
issueCount
pageInfo { hasNextPage endCursor }
nodes {
... on Issue {
number title url author { login }
reactions(content: THUMBS_UP, first: 100) { nodes { user { login } } }
comments(first: 100) {
totalCount
nodes { body reactions(content: THUMBS_UP, first: 100) { nodes { user { login } } } }
}
}
}
}
}
"""
def voters(reactions):
return {r["user"]["login"] for r in reactions["nodes"] if r["user"]}
rows, after, total, truncated = [], None, 0, 0
while True:
args = ["gh", "api", "graphql", "-f", f"query={QUERY}", "-f", f"q={SEARCH}"]
if after:
args += ["-f", f"after={after}"]
page = json.loads(subprocess.check_output(args))["data"]["search"]
total = page["issueCount"]
for issue in page["nodes"]:
body = voters(issue["reactions"])
poll, has_poll = set(), False
for comment in issue["comments"]["nodes"]:
if POLL_MARKER in (comment["body"] or ""):
has_poll = True
poll |= voters(comment["reactions"])
if issue["comments"]["totalCount"] > 100:
truncated += 1
author = (issue["author"] or {}).get("login")
votes = len((body | poll) - {author, MAINTAINER})
rows.append((votes, len(body), len(poll) if has_poll else None, issue["number"], issue["title"], issue["url"]))
if not page["pageInfo"]["hasNextPage"]:
break
after = page["pageInfo"]["endCursor"]
rows.sort(key=lambda r: (-r[0], -r[1], r[3]))
limit = int(os.environ["LIMIT"])
shown = rows[:limit] if limit > 0 else rows
print(f"{total} open enhancement issues, {sum(r[2] is not None for r in rows)} with a poll comment, "
f"{sum(r[0] == 0 for r in rows)} without outside votes")
if truncated:
print(f"Note: {truncated} issues have more than 100 comments; only the first 100 were checked")
print()
if os.environ["FORMAT"] == "md":
print("| votes | on issue | on poll | issue |\n|---:|---:|---:|---|")
for votes, body, poll, number, title, url in shown:
print(f"| {votes} | {body} | {'-' if poll is None else poll} | [#{number}]({url}) {title.replace('|', '/')} |")
else:
print(f"{'votes':>5} {'on issue':>8} {'on poll':>7} issue")
for votes, body, poll, number, title, _ in shown:
print(f"{votes:>5} {body:>8} {'-' if poll is None else poll:>7} #{number} {title}")
PY
+3 -3
View File
@@ -5141,9 +5141,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.3.tgz",
"integrity": "sha512-n+mUVyUX5bVv7G/G2zyIHOhdxfuU1dY2NOFzTQUWiMUbFss8b57NFlgCCaggU78wSw5KVS9cllzeLyzyR+n5nw==",
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz",
"integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==",
"dev": true,
"funding": [
{
+1 -1
View File
@@ -61,7 +61,7 @@
"overrides": {
"minimatch": "^10.2.1",
"brace-expansion": "^5.0.9",
"js-yaml": "^5.2.3",
"js-yaml": "^5.4.1",
"nanoid": "^3.3.18",
"react-router": "7.18.2"
},