Skip to content

[audit] Harden plugin route patterns, i18n keys, and container user - #500

Closed
BillyOutlast wants to merge 256 commits into
Drop-OSS:developfrom
Heretek-Games:fix/semgrep-core-findings
Closed

BillyOutlast wants to merge 256 commits into
Drop-OSS:developfrom
Heretek-Games:fix/semgrep-core-findings

Conversation

@BillyOutlast

Copy link
Copy Markdown
Contributor

Partially remediates #68 (audit finding F-11, Medium).

Changes

  • Plugin route pattern ReDoSpatternToRegex now builds the pattern token-by-token and escapes every literal character (plus a 512-char cap), so a plugin-supplied pattern like /literal/(a+)+$ can no longer inject regex metacharacters. New test asserts the literal path matches and the regex-equivalent path does not.
  • Prototype pollutionfetchLocalisation/deleteLocalisation reject __proto__/constructor/prototype segments and use own-property checks instead of prototype-chain access.
  • Root container — the runtime image now runs as unprivileged uid/gid 10001 instead of root (hadolint clean).

Not changed (verified non-issues)

  • server/composables/ws.ts already selects wss:// when location.protocol === "https:"; semgrep flags the fallback literal.
  • The 8 v-html sites render micromark(...) output, which escapes raw HTML and strips dangerous URL protocols by default (verified: <img onerror> → escaped, javascript: link → href=""). No sanitizer dependency exists in the tree; adding one is a separate decision.

Verification

  • pnpm --filter drop run typecheck clean.
  • pnpm --filter drop run test: 22/22 files pass.
  • pre-commit (prettier/eslint/hadolint/ast-grep/gitleaks) and pre-push (typecheck/knip) hooks green.

Container note

Operators using bind-mounted /data or /library must make them writable by uid 10001 (or run with a matching --user); the image pre-creates and owns them.

John Smith and others added 30 commits August 26, 2026 03:14
- fsBackend.ts: open the file handle first and perform all subsequent
  operations (stat, read stream, write) through that same fd, so there
  is no window between checking a path and using it
- steam.ts: neutralize HTML comments with one regex that consumes both
  closed and unterminated comment openers; after entity decoding, strip
  any resurrected tag sequences so output can never contain live markup

Verified: typecheck clean, build passes, eslint unchanged vs baseline,
ast-grep clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…data

CodeQL flagged that a decoded string could still contain '<script' (an
unterminated tag survives complete-tag stripping). Since legitimate
markup is already converted to Markdown earlier in the pipeline, remove
every remaining '<' outright so no tag opener can exist in the output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CodeQL's model doesn't credit downstream character removal, so restructure
the decoder to never emit '<' or '>' in the first place:
- named bracket entities (&lt;, &gt;) are excluded from the decode set and
  dropped outright
- numeric/hex references encoding 0x3C/0x3E are dropped as well

Adversarial tests confirm no input form (named, double-escaped, decimal,
hex, zero-padded, raw tags, comment tricks) can leave a '<' in the output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Server CI (Lint job):
- pages/account/index.vue: remove commented-out template block that
  tripped vue/no-multiple-template-root under the updated eslint-plugin-
  vue from lockfile drift; note the drafted layout lives in git history
- metadata/igdb.ts: replace dead initializer assignments flagged by
  no-useless-assignment with definite-assignment declarations
- metadata/index.ts: drop redundant '= undefined' initializer

Security workflow:
- gitleaks: invoke the CLI directly instead of gitleaks-action, which
  requires a paid license for organization repos
- cargo-audit: generate Cargo.lock on the fly for vendored crates that
  gitignore it (libarchive, native_model)

Verified locally: eslint 0 errors (11 pre-existing warnings), typecheck
clean, full build passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hing

CodeQL models the /<[^>]*>/g tag-matching regex as incomplete
multi-character sanitization (a '<' outside a matched tag survives).
Replace it with direct removal of every '<' character — legitimate
markup is already Markdown at this point in the pipeline, so removing
the character itself is complete by construction and satisfies the
sanitizer model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bumps the cargo group with 2 updates in the /desktop/src-tauri/client directory: [bytes](https://github.com/tokio-rs/bytes) and [rand](https://github.com/rust-random/rand).


Updates `bytes` from 1.10.1 to 1.12.1
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](tokio-rs/bytes@v1.10.1...v1.12.1)

Updates `rand` from 0.8.5 to 0.8.8
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/0.8.8/CHANGELOG.md)
- [Commits](rust-random/rand@0.8.5...0.8.8)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.12.1
  dependency-type: indirect
- dependency-name: rand
  dependency-version: 0.8.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore: gitignore SonarCloud issue dump and local fix scripts

* fix(sast): remediate SonarCloud mechanical issues (S9011, S6822, S7772, S2933, S1128)

- add explicit type="button" to bare vue/tsx buttons (S9011)
- drop redundant role="list" on ul/ol (Web:S6822)
- prefix node: builtin imports (S7772)
- mark never-modified class members readonly via flows (S2933)
- prune unused named imports (S1128)

Covers 89+25+35+32+30 = 211 of 680 open issues.

* fix(sast): remediate SonarCloud optional-chain/refactor rules

- S6582: nullish-guard -> optional chain (28)
- S6759: React props inline types -> Readonly<Props> (35)
- S7754: findIndex/find-filter .length -> .some (17)
- S3626: remove redundant trailing continue/return (16)
- S7752: .map().flat() -> .flatMap (10)
- S7723: Array() -> new Array (6)
- S7755: [len-1] -> .at(-1) (with non-null assertion in lastItem)
- S7750: .filter().at(0) -> .find (1)

* fix(sast): shell, Docker, and workflow hardening

- shelldre:S7688: if [ .. ] -> [[ .. ]] in clippy-changed.sh + version_update.sh (6)
- docker:S7018: alphabetical apt package sort in Dockerfile (2)
- docker/githubactions:S6505: add --ignore-scripts to pnpm install runs (5 workflow + 1 Dockerfile)

* fix(sast): remediate SonarCloud a11y and small-refactor rules (Phase 3)

* fix(sast): Phase 4 code-able security hardening

- typescript:S4790: md5 -> sha256 in server/internal/library/import tasks + fsBackend object hash (non-cryptographic use, no value dependency)
- swift:S1481: remove unused status local in appletrust/add-certificate.swift
- swift:S1066: merge nested if-let in getCertificateFromString
- javascript:S7767: | 0 -> Math.trunc in base32 pad calc
- javascript:S7751: [].concat(...) -> .flatMap(...) in base32 decoder
- shell:S6506: add --max-redirect=5 to wget in optimize-appimage.sh

Lockfile policy (text:S8570/S8549), prisma migrations (plsql), TODOs (S1135),
SHA-pinning (S7637), regex backtracking (S8786), PATH-taint (S4036), and the
OIDC-Spec URI (S5332) are addressed by SonarCloud-side wontfix accepts in
the following commit.

---------

Co-authored-by: John Smith <you@example.com>
Sync Heretek-AI/drop develop with Drop-OSS/drop@344b89c5.

Resolutions:
- CI workflows: take upstream wholesale (checkout v7, pnpm v6,
  setup-node v7, docker action bumps, devcontainer/CI pipeline fixes)
- server/package.json: adopt upstream placement + simplewebauthn ^13.3.2;
  keep Prisma 7.10.0
- server/internal/metadata/steam.ts: keep hardened single-pass entity
  decoder; adopt full_description undefined fix
- .gitignore: union of both sides
- pnpm-lock.yaml regenerated (security overrides preserved via
  pnpm-workspace.yaml)
- scripts/clippy-changed.sh: exclude desktop/src-tauri per documented intent
…Dockerfile alignment

- Pin all GitHub Actions in workflow files to immutable commit SHAs with version comments
- Generalize server release workflow REGISTRY_IMAGE to ghcr.io/${{ github.repository }}
- Restore non-blocking knip report job in server-ci.yml
- Align Dockerfile global Prisma version with server/package.json (7.10.0)
- Add .git-blame-ignore-revs for repo-wide Prettier formatting commit
- Add historic commit fingerprint to .gitleaksignore
… capability sandboxing, and Settings UI

- Add dynamic plugin discovery scanning ${dataDir}/plugins/*/drop-plugin.json
- Implement plugin state persistence in _state.json with togglePlugin enabling/disabling
- Implement capability sandboxing (routes, storage, websocket, events, network)
- Add admin API endpoints for toggling plugin state and reloading external plugins
- Create Settings > Plugins page in desktop client with capability badges, toggle switches, and reload controls
- Add unit tests verifying plugin enable/disable lifecycle and capability enforcement
…ease workflow

- Fix invalid job dependency in 'winget-publish' from 'build' to 'publish-tauri'
- Elevate WINGET_TOKEN to job level to enable step-level conditional evaluation
- Fix shellcheck quoting and array expansion in macOS certificate signing steps
- Verify workflow cleanly passes actionlint with 0 errors or warnings
- Document monorepo structure across Nuxt/Nitro, Go, Tauri, and Rust crates
- Outline core architectural systems: unified High-DPI Tauri window model, Server Plugin SPI, dynamic discovery, capability sandboxing, and LaunchInterceptor pipeline
- Document toolchain requirements, nightly Rust caveats, and Prisma conventions
- Define local quality gates, Lefthook pre-commit/pre-push hooks, and essential verification commands
…gine

- Classify game distributions (Scene RARs/ISOs, GOG multi-bin, FitGirl/KaOs repacks, 7z/zip archives, loose portable)
- Score and rank game executables over crash handlers and installers
- Generate declarative extraction recipes with standalone setup scripts
- Attach pipeline recipes directly to GameVersion dropletManifests
- Provide maintenance tool to repair placeholder launch and setup configurations
- Strictly maintain zero in-place mutation on seeding storage
- Execute server-generated setup recipes in Rust (7z/innoextract) with streamed progress events.
- Add GameSetupModal with stepper, live log, cancellation and archive reclamation.
- Register start/cancel/reclaim pipeline IPC commands and fall back to legacy setup when a version has no recipe.
- Fix standalone Scene ISO recipes to skip the redundant RAR extraction step.
- Persist discovered unimported games in a new DiscoveredGame model with inferred distribution type.
- Add import:discover and import:bulk task groups plus discovery/import APIs.
- Auto-create discovered games and import their versions with generated recipes.
- Add /admin/import/bulk review table with scan, select, import and ignore actions.
- Cache plaintext depot chunks by SHA-256 checksum with an LRU byte budget (CHUNK_CACHE_DIR, CHUNK_CACHE_MAX_BYTES; disabled by default).
- Serve cache hits directly and fill best-effort on misses, falling back to source storage on any failure.
- Best-effort single-flight fills, atomic publish and startup orphan cleanup.
- Document the optional mount/env and benchmark the hot path.
- Add AGENTS.md as the canonical contributor and AI agent guide covering the monorepo, pipeline runner, bulk auto-import, chunk cache, quality gates and environment quirks.
- Reduce CLAUDE.md and GEMINI.md to tool-specific wrappers that point at AGENTS.md.
- Correct stale architecture, commands and the hook escape-hatch policy.
Operator::new now returns the operator directly, so drop the removed
.finish() call that broke the build. Remove the unused
interactive_optional_variable macro/helper, the dead webbrowser
dependency, and a discarded v2_manifest binding.
Use write_all to avoid the deny-level unused_io_amount error and drop
the no-op "torrential download" scaffolding (empty async fn plus an
unused 312 MB temp-file generator).
Delete files with no remaining references (consts, recursivedirs,
giantbomb, GameCarousel, RankingList), de-export symbols used only
internally, drop unused packages, and correct the prisma client type
import. Guard against unresolved companies in the Steam provider that
the import fix surfaced.
Narrow useGame's status to Ref<GameStatus> under noUncheckedIndexedAccess
and guard optional index access across the library, queue, and settings
views. Replace defineNuxtConfig in both configs with satisfies NuxtConfig
to work around a broken upstream type when @nuxt/schema is hoisted.
Remove unused droplet dependencies (dyn-clone, serde_json), apply clippy
suggestions (Path over PathBuf, derived Default, match ergonomics, field
init shorthand, documented unsafe fns, annotated transmute), and sync
lockfiles.
… lints

Move rust-protobuf output out of the source tree and include it via a
hand-written src/proto/mod.rs that applies clippy/allow attributes. This
stops generated code from tripping the crate's pedantic lints (previously
~184 warnings) and removes the generate-into-src churn.
- Fall back to source streaming when a cache entry races eviction or has
  the wrong size, instead of returning 500.
- Account for cache-fill readers in the file-descriptor semaphore.
- Share DownloadContext behind Arc so no DashMap guard is held across
  awaits.
- Cap RPC frame length, add a response timeout, and recover from bad
  frames instead of panicking.
- Replace panics on peer/manifest data with error propagation.
- Keep the chunk cache directory/files private and reject an unbounded
  (0-byte) budget; keep the index consistent on re-insert.
- Clear clippy/pedantic warnings and add regression tests.
…in CI

Add a shared deny.toml (advisories, licenses, bans, sources) and a
Security workflow job that runs cargo deny over torrential. Declare
AGPL-3.0-only on torrential and droplet_types so the license check can
resolve workspace crates.

Now that torrential clippy is clean, run fmt + clippy in torrential CI
and refresh the contributor docs.
…emediation

fix(torrential): static-analysis remediation (clippy, cache/RPC hardening, cargo-deny)
John Smith added 28 commits September 14, 2026 12:47
Phase 3 / #19: the dropworks-sdk reference clients expect a server API
that did not exist.

- DropworksManager exchanges an appId + Drop API token for a session and
  unlocks achievements, always deriving the user from the token.
- POST /api/v1/dropworks/session -> { userId, gameId, appId }.
- POST /api/v1/dropworks/achievement -> unlocks by stable key; a
  client-supplied userId must match the authenticated session (no
  cross-user spoofing).
- Token from body authToken or Authorization: Bearer; appId resolves by
  Drop game id or external metadata id.
- 3 manager tests (session resolution, rejection paths, unlock auth).
Phase 4 / #20 community hubs (forums were not started):

- Prisma ForumThread / ForumPost models + migration and Game/User
  back-relations.
- ForumManager with create/list/get/reply (locked threads reject) and
  delete, plus title/body length caps.
- Public GET routes for threads and a thread with its replies; client
  routes for creating, replying and deleting (author or admin only).
- 4 manager tests (validation, caps, locking, deletion).
Completes the forums UI for #20:

- Tauri commands fetch_forum_threads / fetch_forum_thread read the public
  community API and deserialize into camelCase views.
- community.vue loads reviews and threads together and renders a
  discussion section with an inline thread view.
- Registered the new commands alongside fetch_game_reviews.
Phase 1 / #17 automatic peer discovery on local subnets:

- New lan::mdns module: PTR query builder, a tolerant mDNS response
  parser (A/AAAA/PTR/SRV/TXT, compression pointers, truncation-safe) and
  SRV+A/AAAA peer resolution into LanPeer.
- MdnsDiscovery mirrors SsdpDiscovery (bind/target/timeout, loopback
  testable) and exposes discover_peers().
- 5 tests (query round-trip, SRV+A resolution, compression pointers,
  truncated packets, loopback responder).
Phase 5 / #21 indie commerce:

- Prisma PurchaseOrder model + migration with User/Game back-relations.
- CommerceManager creates orders through a plugin PaymentGateway's
  createPaymentIntent and, on a succeeded webhook, issues exactly one
  Ed25519 ownership receipt (idempotent re-delivery) using a fail-closed
  operator key (DROP_RECEIPT_SIGNING_KEY).
- Client routes: POST /orders, GET /receipts, GET /receipts/:orderid.
- The payment webhook now settles orders (ignoring sandbox events).
- 4 manager tests (validation/intent, idempotent issuance, failure
  statuses, owner-scoped receipts).
Phase 2 / #18 Drop Input:

- New input::uinput module: state_to_events maps sticks/triggers/dpad to
  ABS events and emits every button's pressed/released state each frame.
- UinputBackend opens /dev/uinput, configures EV_KEY/EV_ABS bits and axis
  ranges, creates the device, pushes input_event frames with SYN_REPORT,
  and destroys the device. create() surfaces the OS error when /dev/uinput
  is unavailable rather than faking a device.
- supported_backends() now reports Uinput on Linux alongside the Null
  fallback; BackendKind docs updated.
- 4 new tests (axis scaling/clamping, button+release/dpad, pre-create
  push error); 10 total.
Phase 5 / #21 developer publishing:

- Prisma GamePrice model + migration and a Game back-relation.
- PricingManager sets/lists prices and resolves only active tiers
  (minor units; 0 = free), with currency/amount validation.
- Public GET /api/v1/store/:gameid/price?currency= and admin-only
  PUT /api/v1/client/commerce/prices/:gameid.
- 3 manager tests (upsert, validation, active-only resolution).
Phase 4 / #20 community hubs (screenshot galleries):

- ObjectHandler.setPermissions replaces an object's permission list.
- ScreenshotManager gains getPublicAllByGame and setVisibility, which
  synchronize object permissions (owner read; anonymous:read when public)
  with the screenshot's visibility.
- Public GET /api/v1/community/:gameid/screenshots and owner-only
  POST /api/v1/client/screenshots/:id/visibility.
- Desktop community page renders the gallery via the existing object
  asset protocol; new fetch_game_screenshots Tauri command.
- 2 tests for the permission mapping.
Phase 3 / #19: extend the native SDK REST contract with leaderboards.

- DropworksManager.submitScore authenticates the token, checks an optional
  claimed userId, resolves the appId, and records the best score via the
  achievements leaderboard service.
- POST /api/v1/dropworks/leaderboard (authenticated) and public
  GET /api/v1/dropworks/leaderboard/:appid/:key.
- 1 new manager test (auth, spoof rejection, validation, delegation).
Phase 5 / #21 developer portal:

- Prisma CrashReport model + migration (nullable user, Game/User
  back-relations).
- TelemetryManager ingests bounded, NUL-stripped reports and lists/counts
  per game.
- Client POST /api/v1/client/telemetry/crash and admin-only
  GET /api/v1/client/telemetry/:gameid/crashes.
- 3 manager tests (sanitization, validation/caps, ordering + counts).
Phase 4 / #20 rich presence (core owns local presence):

- Prisma UserPresence model + migration (status, optional game, freshness
  timestamp) with a User relation.
- PresenceManager upserts presence (validated statuses), clears it, and
  lists fresh non-offline users joined with their profiles.
- Client routes PUT/DELETE /api/v1/client/presence and
  GET /api/v1/client/presence/online.
- 3 manager tests (upsert/validation, clear, online filtering + join).
Completes the rich-presence loop for #20:

- Tauri commands report_presence / clear_presence call the authenticated
  client presence API and no-op quietly when unauthenticated.
- The library page reports in-game with the game id after a successful
  launch and back online when the game is stopped.
Phase 3 / #19: native games can now set rich presence.

- DropworksManager.setPresence authenticates the token, resolves the
  appId, validates the optional claimed userId, and forwards to the local
  presence service (which validates the status).
- POST /api/v1/dropworks/presence with bearer or body authToken.
- 1 new manager test (app resolution, override, auth/spoof/validation).
Phase 3 / #19 NAT traversal foundation:

- webrtc/ice module builds STUN servers from DROP_STUN_URLS and ephemeral
  coturn TURN credentials (HMAC-SHA1 over an expiry:user id, from
  DROP_TURN_SECRET, TTL DROP_TURN_TTL_SECONDS) when both are configured.
- GET /api/v1/client/ice returns the ICE servers for the authenticated
  user.
- 5 tests (url parsing, STUN-only, credential derivation, secret
  required, ttl minimum).
Phase 3 / #19 NAT traversal (client half):

- internal/webrtc/peer.ts wraps the webview RTCPeerConnection: offer/
  answer/ICE flow over a data channel, plus pure signaling/ICE helpers.
- useWebRtc composable fetches ICE config and builds a peer; the
  fetch_ice_config Tauri command performs the authenticated GET.
- Desktop tests cover the pure helpers (11 total); typecheck/lint clean
  and cargo check -p drop-app passes.
Phase 4 / #20 mod manager core:

- internal/workshop/planner.ts computes a dependency-first load order,
  reports missing dependencies, and detects file conflicts between mods.
- 5 tests (ordering, missing deps, cycles, conflicts, disjoint files);
  desktop suite now 16 tests, typecheck clean.
The desktop mod manager needs the subscribed mod's key and gameId to
fetch its manifest and plan a load order; listSubscriptions now joins the
Mod relation. Existing tests assert the join.
Completes the desktop mod-manager loop for #20:

- fetch_workshop_subscriptions / fetch_workshop_mod Tauri commands.
- useWorkshopMods fetches subscriptions, reads each mod manifest, and
  plans the load order.
- pages/workshop.vue shows the ordered mods, missing dependencies and
  file conflicts.
- Desktop typecheck/lint/tests clean and cargo check -p drop-app passes.
Phase 2 / #18 per-game input configurations:

- input::profile: ControllerProfile (name, deadzone, gyro sensitivity,
  button remaps) with serde JSON round-trip, validate_profile, remap and
  apply_profile (deadzone + gyro scale + remaps).
- 4 tests (JSON round-trip, validation errors, apply, remap dedupe);
  clippy clean and cargo check -p drop-app passes.
Phase 2 / #18 handheld power management:

- New power crate: PowerPolicy/PowerState/PowerDecision with a pure
  decide() and a Linux sysfs battery reader (parse_linux_power_state +
  read_linux_power_state).
- download_power_decision Tauri command maps the current battery state to
  allow/throttle/pause for the download manager (unrestricted when
  unknown).
- 5 tests (mains/charging, thresholds, clamping, validation, sysfs
  parsing); clippy clean and cargo check -p drop-app passes.
- Escape regex metacharacters when compiling plugin route patterns and cap the
  pattern length, so a plugin-supplied pattern cannot trigger ReDoS
  (semgrep detect-non-literal-regexp).
- Guard localisation key traversal against __proto__/constructor/prototype and
  use own-property checks (semgrep prototype-pollution-loop).
- Run the runtime image as an unprivileged user (uid 10001) instead of root
  (semgrep missing-user).

Refs #68
@BillyOutlast

Copy link
Copy Markdown
Contributor Author

Opened against the wrong base by mistake while working through an internal audit remediation; the intended PR is against the Heretek-Games fork. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant