--- phase: 18-desktop-client-fertigstellen reviewed: 2026-09-16T00:00:00Z depth: standard files_reviewed: 23 files_reviewed_list: - apps/api/src/desktop/desktop.controller.ts - apps/api/src/desktop/desktop.service.ts - apps/api/src/desktop/desktop.module.ts - apps/api/src/desktop/desktop.service.spec.ts - apps/api/src/app.module.ts - apps/api/Dockerfile - apps/desktop/src-tauri/src/lib.rs - apps/desktop/src/setup.html - apps/desktop/src-tauri/capabilities/default.json - apps/desktop/src-tauri/tauri.conf.json - apps/desktop/src-tauri/Cargo.toml - apps/web/src/lib/desktop.ts - apps/web/src/lib/desktop.test.ts - apps/web/src/components/desktop/desktop-download-links.tsx - apps/web/src/components/settings/desktop-app-settings.tsx - apps/web/src/components/settings/settings-sidebar.tsx - apps/web/src/app/(auth)/login/page.tsx - apps/web/src/app/(portal)/settings/general/desktop/page.tsx - .gitea/workflows/ci.yml - .gitea/scripts/desktop-collect.sh - .gitea/scripts/desktop-version.sh - .gitea/scripts/publish-images.sh - .gitea/scripts/publish-release.sh findings: critical: 1 warning: 3 info: 2 total: 6 status: clean fixed_at: 2026-09-16T17:40:00Z fix_report: 18-REVIEW-FIX.md --- # Phase 18: Code Review Report **Reviewed:** 2026-09-16T00:00:00Z **Depth:** standard **Files Reviewed:** 23 **Status:** clean (all Critical/Warning findings fixed — see `18-REVIEW-FIX.md`) ## Summary Reviewed the desktop-client-fertigstellen phase: the new `apps/api/src/desktop/` module (public `latest`/`download` routes), the Tauri client's server-address setup flow (`check_server`/`save_server_url`, tray/update UI in `lib.rs`), the web download surfaces (login page link, Settings → Allgemein → Desktop-App), and the CI/CD pipeline that builds, collects, and publishes the desktop packages (`ci.yml`, `desktop-collect.sh`, `desktop-version.sh`, `publish-images.sh`, `publish-release.sh`). Overall the phase is careful about the things it calls out as security-sensitive: the CI scripts build all JSON with `jq -n`/`--arg` (no manual string concatenation), the Gitea release token is only ever passed to curl via a header file (never on the command line or in a URL), temp files holding the token are created under a `umask 077` directory, and the HTTP-facing platform parameter on `GET /desktop/download/:platform` is whitelisted before any filesystem access (verified against real path-traversal-style HTTP requests in `desktop.service.spec.ts` Test 4). The Tauri capability/CSP surface and version-check flow largely match the locked decisions in `18-CONTEXT.md` (D-08 no network dependency, D-09 no signing). One genuine gap was found in the second-layer defense against a tampered `manifest.json` (`desktop.service.ts`, D-10/T-18-02): the "defense in depth" filename regex does not reject filenames composed only of dots, so an entry name of `".."` passes the check and `path.join()`s outside `desktop-dist/`. This is not reachable from the public HTTP request today (the manifest is CI-written, not request-controlled), but it is precisely the case the code's own comment says this check exists to block, and it should be fixed to actually do so, especially since the same guard pattern may get reused elsewhere. Three warnings and two info items round out the rest of the findings — none of them break the stated D-10/D-08/D-09 decisions on their own, but they're worth cleaning up. ## Critical Issues ### CR-01: Path-traversal defense-in-depth regex accepts dot-only filenames **File:** `apps/api/src/desktop/desktop.service.ts:111` **Issue:** Step (4) is documented as "Verteidigung in der Tiefe (T-18-02): auch ein manipuliertes Manifest darf nicht aus dem Ordner hinausfuehren" — the whole point is that even if `manifest.json`'s `files[platform].name` were corrupted/attacker-influenced, the regex should stop it from resolving outside `desktopDistDir`. The regex used is: ```ts if (!/^[A-Za-z0-9._-]+$/.test(entry.name)) { throw new NotFoundException(`No package for platform: ${knownPlatform}`); } ``` `.` and `-` are both allowed characters, so a name of exactly `".."` (or `"."`, `"..."`, etc.) passes this test — it contains only characters from the allowed class. Verified directly: ``` node -e "console.log(/^[A-Za-z0-9._-]+$/.test('..'))" // true node -e "console.log(require('path').join('/app/desktop-dist','..'))" // '/app' ``` With `entry.name === '..'`, `path.join(this.desktopDistDir, entry.name)` resolves to the *parent* of `desktop-dist/` (e.g. `/app` in the container image). `fs.existsSync('/app')` is `true` (it's a directory), so the code proceeds to `fs.createReadStream('/app')`, which will emit an `EISDIR` stream error rather than serving a file — not a full data-exfiltration primitive by itself, but it is a real escape of the intended containment boundary, defeats the explicitly-documented guarantee, and produces an unhandled stream-error path (headers already sent) instead of the intended 404. The unit test suite (`desktop.service.spec.ts` Test 7) only exercises `"../x.AppImage"` (rejected because of the `/`), not a bare `".."`/`"."`, so this gap has no test coverage either. Current exploitability requires `manifest.json` itself to be corrupted or attacker-controlled (today it is written exclusively by `desktop-collect.sh` in CI), so the live attack surface is currently narrow — but the code and the phase's own decision record (D-10, T-18-02) both frame this exact line as the safety net for that scenario, and it doesn't hold. **Fix:** Don't rely on a character whitelist alone; verify the resolved path is still inside `desktopDistDir`, and/or explicitly reject `.`/`..` segments: ```ts if ( !/^[A-Za-z0-9._-]+$/.test(entry.name) || entry.name === '.' || entry.name === '..' ) { throw new NotFoundException(`No package for platform: ${knownPlatform}`); } const filePath = path.join(this.desktopDistDir, entry.name); const resolvedRoot = path.resolve(this.desktopDistDir) + path.sep; if (!path.resolve(filePath).startsWith(resolvedRoot)) { throw new NotFoundException(`No package for platform: ${knownPlatform}`); } ``` Add a unit test asserting `entry.name: '..'` and `entry.name: '.'` in the manifest both yield 404 (mirroring the existing Test 7 for `"../x.AppImage"`). **Status:** fixed — commit `0d5c80f`. `.`/`..` are now rejected explicitly and the resolved path is additionally checked against `desktopDistDir`. Added Test 7a/7b/7c (`..`, `.`, `../manifest.json`) to `desktop.service.spec.ts`. See `18-REVIEW-FIX.md`. ## Warnings ### WR-01: Tauri CSP grants `'unsafe-eval'` and wildcard sources that are never needed **File:** `apps/desktop/src-tauri/tauri.conf.json:24` **Issue:** `app.security.csp` is: ```json "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; img-src * data:; font-src * data:; style-src 'self' 'unsafe-inline' *; script-src 'self' 'unsafe-inline' 'unsafe-eval'" ``` This CSP applies to the app's own bundled page (`apps/desktop/src/setup.html`) — the only local page the app serves. That page is a static, inline `