diff --git a/CHANGELOG.md b/CHANGELOG.md index c912cb6fa..2bb9f66bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to Bambuddy will be documented in this file. - **Vite 7 → 8 major bump** — Bambuddy's frontend now builds with Vite 8 (`^7.3.2` → `^8.0.16`) and the matching plugin-react release (`@vitejs/plugin-react` `^5.1.1` → `^5.2.0`). Headline architectural change: Vite 8 swaps Rollup for **Rolldown** as the default bundler — same plugin contract, Rust-backed core, slightly different chunk layout / output bytes (no functional regression). The bump also lifts the transitive `esbuild` floor to 0.28.1, which closes the last open advisory in the audit chain. **Bambuddy-side surface audited:** `vite.config.ts` uses only stable contracts that survived the v8 cut — `defineConfig`, the `Connect` type, the custom `serveGcodeViewer` `configureServer` middleware plugin (proxies `/gcode-viewer/*` to the repo's sibling `gcode_viewer/` directory in dev), the `server.proxy` with WebSocket upgrade for `/api/v1/ws`, `build.outDir`/`emptyOutDir`/`chunkSizeWarningLimit`, and `resolve.alias` for `@`. `base: '/'` regression guard from #1221 is unaffected. No SSR, no library mode, no CSS preprocessors, no exotic plugins. `vitest@4.1.8` already accepts vite 8 in its peer range (`^6 || ^7 || ^8`); no test-runner bump required. **Node:** vite 8 requires `^20.19.0 || >=22.12.0`; CI Node 20.x line satisfies this. **What this is NOT:** plugin-react v6 — that line requires `babel-plugin-react-compiler` + `@rolldown/plugin-babel` as peers and is a separate scope. `npm run build`, `npm run lint`, `npx vitest run` all clean; `npm audit` clean. - **Frontend dependency bumps** — Routine version updates across the runtime, build, and test dependency surface. **Runtime:** `dompurify` 3.4.0 → 3.4.10. `package.json` floor raised from `^3.4.0` to `^3.4.10` so fresh installs cannot land on the deprecated 3.4.4 release. Three call sites use string-output sanitisation (`frontend/src/pages/MakerworldPage.tsx`, `frontend/src/pages/ProjectDetailPage.tsx`, `frontend/src/components/ProjectPageModal.tsx`); release notes 3.4.1 → 3.4.10 reviewed for behavioural changes — 3.4.4 widened the default allow-list with `selectedcontent` + `command` + `commandfor` (all valid modern HTML, harmless for our two default-allow-list call sites), and `ProjectPageModal` is unaffected anyway because it sets an explicit `ALLOWED_TAGS` / `ALLOWED_ATTR` whitelist. **Build / lint / test tooling (transitive, dev-only):** `@babel/core` 7.29.0 → 7.29.7 (pulled by `@vitejs/plugin-react` and `eslint-plugin-react-hooks`), `vite` 7.3.2 → 7.3.5, `markdown-it` 14.1.1 → 14.2.0 (pulled by `@tiptap/extension-link` → `@tiptap/pm` → `prosemirror-markdown`; Bambuddy never calls `markdown-it.render` directly so the change is transparent), `js-yaml` 4.1.1 → 4.2.0 (pulled by `eslint`), `form-data` 4.0.5 → 4.0.6 + `ws` 8.20.1 → 8.21.0 (both pulled by `jsdom` in the test runtime). All bumps inside existing semver ranges except `dompurify`. No source changes required. - **`dompurify` 3.4.10 → 3.4.11** — Follow-up patch closes a moderate-severity advisory affecting `setConfig()` callers: the previous hook clone-guard added in 3.4.7 could be bypassed via `setConfig()`, leaving a permanent `ALLOWED_ATTR` pollution that the next `sanitize()` call inherited. **Bambuddy's exposure is nil** — `git grep DOMPurify.setConfig` returns zero hits across the entire codebase; all three sanitisation sites (`frontend/src/pages/MakerworldPage.tsx`, `frontend/src/pages/ProjectDetailPage.tsx`, `frontend/src/components/ProjectPageModal.tsx`) call `DOMPurify.sanitize(html)` or `DOMPurify.sanitize(html, {ALLOWED_TAGS, ALLOWED_ATTR})` directly, never through `setConfig()`. The bump is taken as defence-in-depth to keep XSS-sensitive surface area current and to silence `npm audit` so future audit-fix runs don't auto-bundle unintended changes. **Mechanical lockfile bump only:** the existing `^3.4.10` range already permitted 3.4.11, so `package.json` is unchanged; `package-lock.json` updates the resolved URL + integrity hash for the one entry. Verification: `npm audit` reports 0 vulnerabilities, `MakerworldPage.test.tsx`'s 12 DOMPurify sanitisation cases pass, `npm run build` clean. +- **Backend dependency security floor raises (cryptography / python-multipart / starlette)** — pip-audit December 2026 cycle surfaced six advisories across three direct deps; floors in `requirements.txt` lifted to the documented fix releases, plus one transitive co-bump for resolver compatibility. **`cryptography` 46.0.7 → 48.0.1 floor** (resolver picks 49.0.0 within the new floor) — clears GHSA-537c-gmf6-5ccf (non-contiguous Python buffer handling that could overflow on APIs accepting buffer protocol input). **Release-notes audit (done before bump):** v47.0.0 dropped Python 3.8 + OpenSSL 1.1.x + binary elliptic curves (SECT*) + Camellia + CFB/OFB/CFB8 modes (moved to `cryptography_decrepit`); v48.0.0 dropped `PUBLIC_KEY_TYPES` / `PRIVATE_KEY_TYPES` type aliases. Bambuddy's grep is clean across every one of those: `core/encryption.py` uses Fernet (AES-128-CBC + HMAC), `services/spoolbuddy_ssh.py` uses ed25519, `services/virtual_printer/certificate.py` uses RSA + x509 + ExtendedKeyUsageOID. Python 3.13 + OpenSSL 3.x on container, so the version-floor bumps are no-ops for us. **`python-multipart` 0.0.27 → 0.0.31 floor** (resolver picks 0.0.32) — clears CVE-2026-53538/53539/53540 in the multipart parser surface (boundary length capped at 256 bytes, RFC 2231 continuation handling, Content-Length non-negative validation, bounded header field name size before validation). **Behavioural changes audited:** 0.0.30 stopped recognising RFC 2231/5987 extended `filename*` / `name*` parameters in incoming bodies — Bambuddy emits these on outgoing Content-Disposition response headers (`utils/http.py:17`) but doesn't parse them on the request side, and clients that include both `filename=` and `filename*=` keep working via the plain `filename=` fallback (slight cosmetic difference for non-ASCII filenames in uploads). 0.0.30 also tightened form-urlencoded parsing to treat only `&` as field separator — every Bambuddy client (browser, BambuStudio, OrcaSlicer) already uses `&`. **`starlette` 1.1.0 → 1.3.1 floor** — clears CVE-2026-54282/54283 (FormParser `max_part_size` / `max_fields` limits now actually enforced after being declared-but-ignored in earlier releases; `StaticFiles.lookup_path` rejects absolute paths; `FileResponse` clamps oversized suffix range requests; `URL.replace()` IndexError fix). **Critical pre-bump check:** the newly-enforced `max_part_size=1MB` default would have broken every file upload (`UploadFile = File(...)` in `inventory.py:1127`, `projects.py:886/1053/1780`, `library.py:1787`, `local_presets.py:82`, `external_links.py:166`, `local_backup.py`) if it applied to file streams. Inspected the `MultiPartParser.on_part_data` source: the size check at `if self._current_part.file is None:` only fires for **text** form fields, not file streams — so file uploads of arbitrary size still pass through unaffected. Text form bodies in Bambuddy are login credentials and similar small values, well under the 1MB ceiling. **Side rename:** `backend/app/api/routes/mfa.py:470/1364/1428` replaces 3 references of `status.HTTP_422_UNPROCESSABLE_ENTITY` (deprecated in starlette 1.3.x) with `HTTP_422_UNPROCESSABLE_CONTENT`. Same 422 wire status; silences the 3 deprecation warnings under our own ownership (the two remaining warnings come from FastAPI internals — upstream's to fix). **`pyopenssl` 26.0.0 → 26.3.0 floor** — **NOT a security fix**; required because pyOpenSSL `<26.3.0` caps `cryptography<47` in its install_requires, so without an explicit floor the resolver either downgrades cryptography below the GHSA-537c-gmf6-5ccf fix line or installs an inconsistent pair (pip's resolver warns but proceeds). Bambuddy has no direct `from OpenSSL ...` imports — pyOpenSSL is pulled transitively by `asyncssh` + `pywebpush`. **Verification:** `pip-audit` clean, `pip check` clean, `ruff check backend/` clean, backend `pytest -n 30` 6167/6167 in 86.55s. No DB migration, no API surface change, no permission change, no frontend change. ### Added - **Centralised sidebar layout + per-page hide toggles (#1673, contributed by @EdwardChamberlain)** — Sidebar item ordering and visibility move from inline `Layout.tsx` state to a dedicated module so the same persistence rules apply whether the user is reordering with drag-and-drop, toggling an item off, or accepting the admin-pushed default. New `frontend/src/utils/sidebarLayout.ts` owns the localStorage round-trip (`sidebarOrder` + `sidebarHiddenSystemItems` keys), the `SIDEBAR_LAYOUT_CHANGED_EVENT` cross-tab refresh broadcast, and the `isExternalSidebarItemId` helper that distinguishes the new `ext-*` external link prefix from built-in nav. **Hide / show toggle:** every built-in sidebar entry (Printers / Inventory / Archives / Queue / Projects / File Manager / Makerworld / Profiles / Maintenance / Statistics — Settings is intentionally non-hideable) now carries an eye icon in the Sidebar settings card; click it to drop that entry from the rendered sidebar. Hidden IDs persist per-user via localStorage so personal taste survives reloads without leaking to other users on a shared install. Re-show by clicking the eye again. The previous drag-to-reorder UX is retired in this PR — the hide list + admin default order cover the same "I never use the Stats page" / "give me Files first" needs without the affordance ambiguity of the rearrange handle. **Admin default order:** new `default_sidebar_order` setting (validated server-side at `backend/app/schemas/settings.py:533+`) holds a JSON object `{order: string[], hiddenSystemItemIds: string[]}` that admins set once from Settings → General → Sidebar (Set Default toggle). On first login per user, `Layout.tsx`'s `useEffect` reads the admin default, filters it against the current `defaultNavItems` + valid external IDs (so a deleted external link or a removed built-in doesn't strand in someone's stored order), applies it locally, and records a per-user `sidebarDefaultApplied_` localStorage flag so the default is one-shot — later user-driven changes aren't clobbered on every login. **Settings card:** `ExternalLinksSettings.tsx` is the single source of truth for the Sidebar card (`card-sidebar-links`) in Settings → General. The header now carries the **Set Default** toggle (visible only when the caller holds `settings:write`), a **Reset** button (clears both `sidebarOrder` + `sidebarHiddenSystemItems` to defaults), and the **Add Link** button (opens the external-link create modal). The body lists every sidebar item — built-in or external — with the eye toggle inline on each row. The header row uses `flex-wrap` on the outer container and the right-side control group so the Add Link button doesn't overflow the card's right edge when Column 3 sits at its narrow `lg:max-w-sm` (384px) width. **Settings → General reordering (post-merge polish):** the **Updates** card moved to the top of Column 3 (above the new Sidebar card); the **Data Management** card moved to the bottom of Column 2 (after Library Auto-Purge) so the General tab balances better with the new Sidebar card taking column 3's vertical real estate. Anchor IDs `card-updates`, `card-data`, `card-sidebar-links` are preserved so deep-links + the in-app `registerSettingsSearch` index still resolve. **Layout merge edge case:** the PR's refactor of `Layout.tsx::isHidden` accidentally dropped the dev-side notifications gate (`!authEnabled || !advancedAuthStatus?.advanced_auth_enabled || settings?.user_notifications_enabled === false`) and its `advancedAuthStatus` useQuery. The merged shape keeps three gates in priority order — `hiddenSystemItemIds.includes(id)` first (cheapest, explicit user intent), then the array-aware `navPermissions` check from #1755 (granular `*:read_own` / `*:read_all` tiers), then the notifications-specific gate — so a user without advanced auth doesn't suddenly see the Notifications entry. **Backend:** `default_sidebar_order` settings field accepts both shapes (plain array OR `{order, hiddenSystemItemIds}` object) for backward compat with installs that saved an array under an earlier draft of this work. Validator rejects any `hiddenSystemItemIds` that isn't a `list[str]` with 422. **Tests:** 17 new backend cases in `test_sidebar_settings.py` pinning the validator (empty / JSON-array / JSON-object / mixed-types / hostile shapes). Frontend: 5 new `Layout.test.tsx` cases pinning the hide-toggle behaviour (hidden ID drops the entry, hidden ID for Settings is ignored — `settings` is non-hideable, eye-click round-trips through localStorage, `SIDEBAR_LAYOUT_CHANGED_EVENT` triggers a re-read across tabs) and 255 added/changed lines in `SettingsPage.test.tsx` covering the admin-default toggle and the eye-icon visibility column. **i18n:** new keys in the `externalLinks.*` namespace (sidebarLayout / sidebarLayoutDescription / visibleInSidebar / hiddenFromSidebar / requiredInSidebar / setDefault / etc.), full translations in all 10 non-en locales (de / es / fr / it / ja / ko / pt-BR / tr / zh-CN / zh-TW). Parity check 5168 leaves per locale. Vitest test timeout raised in `vitest.config.ts` to absorb the `userEvent.setup({delay: null})` cases in the heavier `SettingsPage` flows. Full vitest run green; ESLint clean; `npm run build` clean; ruff clean. diff --git a/backend/app/api/routes/mfa.py b/backend/app/api/routes/mfa.py index 83bda734b..32ca92544 100644 --- a/backend/app/api/routes/mfa.py +++ b/backend/app/api/routes/mfa.py @@ -467,7 +467,7 @@ def _enforce_auto_link_safety(provider: OIDCProvider) -> None: """ if provider.auto_link_existing_accounts and provider.email_claim == "email" and not provider.require_email_verified: raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=AUTO_LINK_REQUIREMENTS_ERROR, ) @@ -1361,7 +1361,7 @@ async def create_oidc_provider( grp_chk = await db.execute(select(Group).where(Group.id == body.default_group_id)) if not grp_chk.scalar_one_or_none(): raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="default_group_id references a non-existent group", ) @@ -1425,7 +1425,7 @@ async def update_oidc_provider( grp_chk = await db.execute(select(Group).where(Group.id == body.default_group_id)) if not grp_chk.scalar_one_or_none(): raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail="default_group_id references a non-existent group", ) diff --git a/requirements.txt b/requirements.txt index 109e68a99..577771607 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,14 @@ aioftp>=0.22.0 # Virtual Printer (emulates Bambu printer for slicer uploads) pyftpdlib>=2.0.0 -cryptography>=46.0.7 +# 46.x line has GHSA-537c-gmf6-5ccf; 48.0.1 is the fix release. Upstream's +# X.509 / PKCS#7 surface is in our trust path via asyncssh, pyOpenSSL, +# py-vapid, http_ece, pywebpush. +cryptography>=48.0.1 +# Transitive of asyncssh / pywebpush. pyopenssl<26.3.0 caps `cryptography<47` +# so without this floor the resolver either downgrades cryptography below +# the GHSA-537c-gmf6-5ccf fix line or installs an inconsistent pair. +pyopenssl>=26.3.0 # SpoolBuddy remote SSH updates (pure-Python SSH client; avoids the # OpenSSH `ssh` binary which calls getpwuid() and fails in Docker when @@ -48,7 +55,9 @@ openpyxl>=3.1.0 pywebpush>=2.0.0 # Utilities -python-multipart>=0.0.27 +# 0.0.27 → 0.0.31 clears three CVEs in the parser surface that FastAPI +# uses for multipart form bodies (CVE-2026-53538/53539/53540). +python-multipart>=0.0.31 aiofiles>=23.0.0 # QR Code generation @@ -112,10 +121,11 @@ curl_cffi>=0.7.0 # would silently keep installing the vulnerable 2.6.x line. urllib3>=2.7.0 -# Transitive of fastapi. starlette 1.0.0 has PYSEC-2026-161; 1.0.1 is the -# fixed release. fastapi's range still admits 1.0.0 so we pin the floor -# directly to stop the resolver from picking the vulnerable build. -starlette>=1.0.1 +# Transitive of fastapi. starlette 1.0.0 has PYSEC-2026-161; 1.1.x has +# CVE-2026-54282/54283; 1.3.1 is the fixed release. fastapi's range still +# admits the vulnerable builds, so we pin the floor directly to stop the +# resolver from picking them. +starlette>=1.3.1 # Transitive of pywebpush (unpinned `aiohttp` requirement). aiohttp 3.13.5 # has CVE-2026-34993 and CVE-2026-47265, both fixed in 3.14.0. pywebpush