From 19823c5498ff4e9f4d5109b2d13a87e4d44eed45 Mon Sep 17 00:00:00 2001 From: issuemakerable Date: Thu, 29 Jan 2026 19:28:33 +0900 Subject: [PATCH] security: add timing-safe comparisons and fix dependency CVEs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create shared safeEqual() utility using timingSafeEqual (src/security/safe-equal.ts) - Fix hook token comparison in server-http.ts to use safeEqual() - Fix node pairing token in node-pairing.ts to use safeEqual() - Fix audit token in audit-extra.ts to use safeEqual() - Refactor gateway auth.ts to import from shared utility - Bump tar 7.5.4→7.5.7 (CVE GHSA-34x7-hfp2-rc4v: hardlink path traversal) - Bump hono 4.11.4→4.11.7 (XSS via ErrorBoundary + static middleware key read) - Add pnpm.overrides for matrix transitive deps (form-data→2.5.5, qs→6.14.1) --- AGENTS.md | 93 +++++++++++++++------- package.json | 12 +-- pnpm-lock.yaml | 149 +++++++----------------------------- src/gateway/auth.ts | 7 +- src/gateway/server-http.ts | 3 +- src/infra/node-pairing.ts | 4 +- src/security/audit-extra.ts | 3 +- src/security/safe-equal.ts | 10 +++ 8 files changed, 116 insertions(+), 165 deletions(-) create mode 100644 src/security/safe-equal.ts diff --git a/AGENTS.md b/AGENTS.md index 44b0149fd..a2583fd64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ - When Peter asks for links, reply with full `https://docs.molt.bot/...` URLs (not root-relative). - When you touch docs, end the reply with the `https://docs.molt.bot/...` URLs you referenced. - README (GitHub): keep absolute docs URLs (`https://docs.molt.bot/...`) so links work on GitHub. -- Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and “gateway host”. +- Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and "gateway host". ## exe.dev VM ops (general) - Access: stable path is `ssh exe.dev` then `ssh vm-name` (assume SSH key already set). @@ -45,32 +45,69 @@ - Node remains supported for running built output (`dist/*`) and production installs. - Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch. Release checklist: `docs/platforms/mac/release.md`. - Type-check/build: `pnpm build` (tsc) -- Lint/format: `pnpm lint` (oxlint), `pnpm format` (oxfmt) -- Tests: `pnpm test` (vitest); coverage: `pnpm test:coverage` +- Lint: `pnpm lint` (oxlint with `--type-aware`). Fix: `pnpm lint:fix`. +- Format check: `pnpm format` (oxfmt `--check`). Fix: `pnpm format:fix` (oxfmt `--write`). +- Tests: `pnpm test` (vitest via parallel runner); coverage: `pnpm test:coverage`. +- Run a **single test file**: `npx vitest run src/path/to/file.test.ts` (or `bunx vitest run ...`). +- Run tests **matching a name**: `npx vitest run -t "test name pattern"`. +- Watch mode: `pnpm test:watch`. +- E2E tests: `pnpm test:e2e` (vitest with `vitest.e2e.config.ts`). +- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live`. +- Full local gate (run before landing PRs): `pnpm lint && pnpm build && pnpm test`. +- CI runs: lint, format check, build (tsc), tests (node + bun), protocol check — all on every push/PR. ## Coding Style & Naming Conventions -- Language: TypeScript (ESM). Prefer strict typing; avoid `any`. -- Formatting/linting via Oxlint and Oxfmt; run `pnpm lint` before commits. +- Language: TypeScript (ESM, `"type": "module"`). `strict: true` in tsconfig. +- Target: ES2022, NodeNext module resolution. +- Formatting/linting via **oxlint** and **oxfmt** (not eslint/prettier); run `pnpm lint` before commits. +- oxlint config: `.oxlintrc.json` — plugins: `unicorn`, `typescript`, `oxc`; correctness category = error. - Add brief code comments for tricky or non-obvious logic. -- Keep files concise; extract helpers instead of “V2” copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`. -- Aim to keep files under ~700 LOC; guideline only (not a hard guardrail). Split/refactor when it improves clarity or testability. +- Keep files concise; extract helpers instead of "V2" copies. Use existing patterns for CLI options and dependency injection via `createDefaultDeps`. +- Aim to keep files under ~500 LOC (guideline, not hard guardrail). `pnpm check:loc` enforces `--max 500`. - Naming: use **Moltbot** for product/app/docs headings; use `moltbot` for CLI command, package/binary, paths, and config keys. +### Import Conventions +- Node built-ins: use `node:` prefix (e.g. `import fs from "node:fs"`, `import path from "node:path"`). +- Order: node built-ins → external packages → local modules (separated by blank lines). +- Use `import type { Foo } from "..."` for type-only imports; inline `type` in mixed imports when only some are types: `import { type Foo, bar } from "..."`. +- Local imports use `.js` extension (ESM requirement): `import { foo } from "./bar.js"`. +- Barrel re-exports allowed in module index files (e.g. `src/config/config.ts` re-exports from submodules). + +### TypeScript Patterns +- Strict mode with `noEmitOnError`. No `any`, `@ts-ignore`, or `@ts-expect-error`. +- Validation: Zod (`zod` v4) for config/schema validation. TypeBox (`@sinclair/typebox`) for tool input schemas. +- Use `as const` for constant arrays/objects; derive types with `typeof X[number]`. +- Avoid `Type.Union` in tool schemas; use `stringEnum`/`optionalStringEnum` (Type.Unsafe enum) for string lists. +- Avoid raw `format` property names in tool schemas (reserved keyword in some validators). +- Prefer `Type.Optional(...)` over `... | null` in tool schemas. + +### Naming +- Files: `kebab-case.ts` (e.g. `session-utils.ts`, `parse-log-line.ts`). +- Functions/variables: `camelCase` (e.g. `loadConfig`, `resolveMainSessionKey`). +- Types/interfaces: `PascalCase` (e.g. `MoltbotConfig`, `SessionEntry`, `ChannelMeta`). +- Constants: `UPPER_SNAKE_CASE` for true constants (e.g. `DEFAULT_LOG_DIR`, `MAX_LOG_AGE_MS`). +- Test files: `*.test.ts` colocated with source. E2E: `*.e2e.test.ts`. Live: `*.live.test.ts`. + +### Error Handling +- Prefer explicit error types; avoid empty `catch {}` blocks. +- Logging errors: use `tslog` via `src/logging/logger.ts`; never hand-roll loggers. +- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don't hand-roll spinners/bars. +- Status output: keep tables + ANSI-safe wrapping (`src/terminal/table.ts`). +- Colors: use shared CLI palette in `src/terminal/palette.ts` (no hardcoded ANSI colors). + +### Testing Patterns +- Framework: Vitest (`describe`/`it`/`expect`). Test timeout: 120s. Pool: `forks`. +- Mocking: `vi.fn()`, `vi.mock()`. Tests use a global setup file (`test/setup.ts`) that installs an isolated test home and plugin registry. +- Assertions: `expect(x).toBe(y)`, `expect(fn).toHaveBeenCalledTimes(n)`, `expect(promise).rejects.toThrow("msg")`. +- Coverage: V8 provider, thresholds 70% lines/functions/statements, 55% branches. +- Do not set test workers above 16. +- Pure test additions/fixes generally do **not** need a changelog entry unless they alter user-facing behavior. + ## Release Channels (Naming) - stable: tagged releases only (e.g. `vYYYY.M.D`), npm dist-tag `latest`. - beta: prerelease tags `vYYYY.M.D-beta.N`, npm dist-tag `beta` (may ship without macOS app). - dev: moving head on `main` (no tag; git checkout main). -## Testing Guidelines -- Framework: Vitest with V8 coverage thresholds (70% lines/branches/functions/statements). -- Naming: match source names with `*.test.ts`; e2e in `*.e2e.test.ts`. -- Run `pnpm test` (or `pnpm test:coverage`) before pushing when you touch logic. -- Do not set test workers above 16; tried already. -- Live tests (real keys): `CLAWDBOT_LIVE_TEST=1 pnpm test:live` (Moltbot-only) or `LIVE=1 pnpm test:live` (includes provider live tests). Docker: `pnpm test:docker:live-models`, `pnpm test:docker:live-gateway`. Onboarding Docker E2E: `pnpm test:docker:onboard`. -- Full kit + what’s covered: `docs/testing.md`. -- Pure test additions/fixes generally do **not** need a changelog entry unless they alter user-facing behavior or the user asks for one. -- Mobile: before using a simulator, check for connected real devices (iOS + Android) and prefer them when available. - ## Commit & Pull Request Guidelines - Create commits with `scripts/committer "" `; avoid manual `git add`/`git commit` so staging stays scoped. - Follow concise, action-oriented commit messages (e.g., `CLI: add verbose flag to send`). @@ -81,12 +118,12 @@ - PR review calls: prefer a single `gh pr view --json ...` to batch metadata/comments; run `gh pr diff` only when needed. - Before starting a review when a GH Issue/PR is pasted: run `git pull`; if there are local changes or unpushed commits, stop and alert the user before reviewing. - Goal: merge PRs. Prefer **rebase** when commits are clean; **squash** when history is messy. -- PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless it’s truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`. +- PR merge flow: create a temp branch from `main`, merge the PR branch into it (prefer squash unless commit history is important; use rebase/merge when it is). Always try to merge the PR unless it's truly difficult, then use another approach. If we squash, add the PR author as a co-contributor. Apply fixes, add changelog entry (include PR # + thanks), run full gate before the final commit, commit, merge back to `main`, delete the temp branch, and end on `main`. - If you review a PR and later do work on it, land via merge/squash (no direct-main commits) and always add the PR author as a co-contributor. - When working on a PR: add a changelog entry with the PR number and thank the contributor. - When working on an issue: reference the issue in the changelog entry. - When merging a PR: leave a PR comment that explains exactly what we did and include the SHA hashes. -- When merging a PR from a new contributor: add their avatar to the README “Thanks to all clawtributors” thumbnail list. +- When merging a PR from a new contributor: add their avatar to the README "Thanks to all clawtributors" thumbnail list. - After merging a PR: run `bun scripts/update-clawtributors.ts` if the contributor is missing, then commit the regenerated README. ## Shorthand Commands @@ -115,15 +152,15 @@ - Never update the Carbon dependency. - Any dependency with `pnpm.patchedDependencies` must use an exact version (no `^`/`~`). - Patching dependencies (pnpm patches, overrides, or vendored changes) requires explicit approval; do not do this by default. -- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don’t hand-roll spinners/bars. +- CLI progress: use `src/cli/progress.ts` (`osc-progress` + `@clack/prompts` spinner); don't hand-roll spinners/bars. - Status output: keep tables + ANSI-safe wrapping (`src/terminal/table.ts`); `status --all` = read-only/pasteable, `status --deep` = probes. - Gateway currently runs only as the menubar app; there is no separate LaunchAgent/helper label installed. Restart via the Moltbot Mac app or `scripts/restart-mac.sh`; to verify/kill use `launchctl print gui/$UID | grep moltbot` rather than assuming a fixed label. **When debugging on macOS, start/stop the gateway via the app, not ad-hoc tmux sessions; kill any temporary tunnels before handoff.** - macOS logs: use `./scripts/clawlog.sh` to query unified logs for the Moltbot subsystem; it supports follow/tail/category filters and expects passwordless sudo for `/usr/bin/log`. - If shared guardrails are available locally, review them; otherwise follow this repo's guidance. -- SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; don’t introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code. +- SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; don't introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code. - Connection providers: when adding a new connection, update every UI surface and docs (macOS app, web UI, mobile if applicable, onboarding/overview docs) and add matching status + configuration forms so provider lists and settings stay in sync. - Version locations: `package.json` (CLI), `apps/android/app/build.gradle.kts` (versionName/versionCode), `apps/ios/Sources/Info.plist` + `apps/ios/Tests/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `apps/macos/Sources/Moltbot/Resources/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `docs/install/updating.md` (pinned npm version), `docs/platforms/mac/release.md` (APP_VERSION/APP_BUILD examples), Peekaboo Xcode projects/Info.plists (MARKETING_VERSION/CURRENT_PROJECT_VERSION). -- **Restart apps:** “restart iOS/Android apps” means rebuild (recompile/install) and relaunch, not just kill/launch. +- **Restart apps:** "restart iOS/Android apps" means rebuild (recompile/install) and relaunch, not just kill/launch. - **Device checks:** before testing, verify connected real devices (iOS/Android) before reaching for simulators/emulators. - iOS Team ID lookup: `security find-identity -p codesigning -v` → use Apple Development (…) TEAMID. Fallback: `defaults read com.apple.dt.Xcode IDEProvisioningTeamIdentifiers`. - A2UI bundle hash: `src/canvas-host/a2ui/.bundle.hash` is auto-generated; ignore unexpected changes, and only regenerate via `pnpm canvas:a2ui:bundle` (or `scripts/bundle-a2ui.sh`) when needed. Commit the hash as a separate commit. @@ -140,19 +177,19 @@ - If commit/push already requested, auto-stage and include formatting-only follow-ups in the same commit (or a tiny follow-up commit if needed), no extra confirmation. - Only ask when changes are semantic (logic/data/behavior). - Lobster seam: use the shared CLI palette in `src/terminal/palette.ts` (no hardcoded colors); apply palette to onboarding/config prompts and other TTY UI output as needed. -- **Multi-agent safety:** focus reports on your edits; avoid guard-rail disclaimers unless truly blocked; when multiple agents touch the same file, continue if safe; end with a brief “other files present” note only if relevant. +- **Multi-agent safety:** focus reports on your edits; avoid guard-rail disclaimers unless truly blocked; when multiple agents touch the same file, continue if safe; end with a brief "other files present" note only if relevant. - Bug investigations: read source code of relevant npm dependencies and all related local code before concluding; aim for high-confidence root cause. - Code style: add brief comments for tricky logic; keep files under ~500 LOC when feasible (split/refactor as needed). - Tool schema guardrails (google-antigravity): avoid `Type.Union` in tool input schemas; no `anyOf`/`oneOf`/`allOf`. Use `stringEnum`/`optionalStringEnum` (Type.Unsafe enum) for string lists, and `Type.Optional(...)` instead of `... | null`. Keep top-level tool schema as `type: "object"` with `properties`. - Tool schema guardrails: avoid raw `format` property names in tool schemas; some validators treat `format` as a reserved keyword and reject the schema. -- When asked to open a “session” file, open the Pi session logs under `~/.clawdbot/agents//sessions/*.jsonl` (use the `agent=` value in the Runtime line of the system prompt; newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there. +- When asked to open a "session" file, open the Pi session logs under `~/.clawdbot/agents//sessions/*.jsonl` (use the `agent=` value in the Runtime line of the system prompt; newest unless a specific ID is given), not the default `sessions.json`. If logs are needed from another machine, SSH via Tailscale and read the same path there. - Do not rebuild the macOS app over SSH; rebuilds must be run directly on the Mac. - Never send streaming/partial replies to external messaging surfaces (WhatsApp, Telegram); only final replies should be delivered there. Streaming/tool events may still go to internal UIs/control channel. - Voice wake forwarding tips: - - Command template should stay `moltbot-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don’t add extra quotes. - - launchd PATH is minimal; ensure the app’s launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`moltbot` binaries resolve when invoked via `moltbot-mac`. -- For manual `moltbot message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool’s escaping. -- Release guardrails: do not change version numbers without operator’s explicit consent; always ask permission before running any npm publish/release step. + - Command template should stay `moltbot-mac agent --message "${text}" --thinking low`; `VoiceWakeForwarder` already shell-escapes `${text}`. Don't add extra quotes. + - launchd PATH is minimal; ensure the app's launch agent PATH includes standard system paths plus your pnpm bin (typically `$HOME/Library/pnpm`) so `pnpm`/`moltbot` binaries resolve when invoked via `moltbot-mac`. +- For manual `moltbot message send` messages that include `!`, use the heredoc pattern noted below to avoid the Bash tool's escaping. +- Release guardrails: do not change version numbers without operator's explicit consent; always ask permission before running any npm publish/release step. ## NPM + 1Password (publish/verify) - Use the 1password skill; all `op` commands must run inside a fresh tmux session. diff --git a/package.json b/package.json index 04322f3af..1dc883b0e 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "express": "^5.2.1", "file-type": "^21.3.0", "grammy": "^1.39.3", - "hono": "4.11.4", + "hono": "4.11.7", "jiti": "^2.6.1", "json5": "^2.2.3", "jszip": "^3.10.1", @@ -201,7 +201,7 @@ "qrcode-terminal": "^0.12.0", "sharp": "^0.34.5", "sqlite-vec": "0.1.7-alpha.2", - "tar": "7.5.4", + "tar": "7.5.7", "tslog": "^4.10.2", "undici": "^7.19.0", "ws": "^8.19.0", @@ -242,14 +242,16 @@ "wireit": "^0.14.12" }, "overrides": { - "tar": "7.5.4" + "tar": "7.5.7" }, "pnpm": { "minimumReleaseAge": 2880, "overrides": { "@sinclair/typebox": "0.34.47", - "hono": "4.11.4", - "tar": "7.5.4" + "hono": "4.11.7", + "tar": "7.5.7", + "request>form-data": "2.5.5", + "request>qs": "6.14.1" } }, "vitest": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c0f99928..e37de149e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,10 @@ settings: overrides: '@sinclair/typebox': 0.34.47 - hono: 4.11.4 - tar: 7.5.4 + hono: 4.11.7 + tar: 7.5.7 + request>form-data: 2.5.5 + request>qs: 6.14.1 importers: @@ -21,7 +23,7 @@ importers: version: 3.975.0 '@buape/carbon': specifier: 0.14.0 - version: 0.14.0(hono@4.11.4) + version: 0.14.0(hono@4.11.7) '@clack/prompts': specifier: ^0.11.0 version: 0.11.0 @@ -110,8 +112,8 @@ importers: specifier: ^1.39.3 version: 1.39.3 hono: - specifier: 4.11.4 - version: 4.11.4 + specifier: 4.11.7 + version: 4.11.7 jiti: specifier: ^2.6.1 version: 2.6.1 @@ -155,8 +157,8 @@ importers: specifier: 0.1.7-alpha.2 version: 0.1.7-alpha.2 tar: - specifier: 7.5.4 - version: 7.5.4 + specifier: 7.5.7 + version: 7.5.7 tslog: specifier: ^4.10.2 version: 4.10.2 @@ -383,12 +385,12 @@ importers: '@microsoft/agents-hosting-extensions-teams': specifier: ^1.2.2 version: 1.2.2 - moltbot: - specifier: workspace:* - version: link:../.. express: specifier: ^5.2.1 version: 5.2.1 + moltbot: + specifier: workspace:* + version: link:../.. proper-lockfile: specifier: ^4.1.2 version: 4.1.2 @@ -1090,7 +1092,7 @@ packages: resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} engines: {node: '>=18.14.1'} peerDependencies: - hono: 4.11.4 + hono: 4.11.7 '@huggingface/jinja@0.5.3': resolution: {integrity: sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA==} @@ -3214,11 +3216,6 @@ packages: class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - clawdbot@2026.1.24-3: - resolution: {integrity: sha512-zt9BzhWXduq8ZZR4rfzQDurQWAgmijTTyPZCQGrn5ew6wCEwhxxEr2/NHG7IlCwcfRsKymsY4se9KMhoNz0JtQ==} - engines: {node: '>=22.12.0'} - hasBin: true - cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} @@ -3632,10 +3629,6 @@ packages: forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} - form-data@2.3.3: - resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} - engines: {node: '>= 0.12'} - form-data@2.5.5: resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} engines: {node: '>= 0.12'} @@ -3793,8 +3786,8 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} - hono@4.11.4: - resolution: {integrity: sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==} + hono@4.11.7: + resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} engines: {node: '>=16.9.0'} hookified@1.15.0: @@ -4809,10 +4802,6 @@ packages: resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} - qs@6.5.3: - resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} - engines: {node: '>=0.6'} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -5190,8 +5179,8 @@ packages: tailwindcss@4.1.17: resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} - tar@7.5.4: - resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==} + tar@7.5.7: + resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} engines: {node: '>=18'} thenify-all@1.6.0: @@ -6433,14 +6422,14 @@ snapshots: '@borewit/text-codec@0.2.1': {} - '@buape/carbon@0.14.0(hono@4.11.4)': + '@buape/carbon@0.14.0(hono@4.11.7)': dependencies: '@types/node': 25.0.10 discord-api-types: 0.38.37 optionalDependencies: '@cloudflare/workers-types': 4.20260120.0 '@discordjs/voice': 0.19.0 - '@hono/node-server': 1.19.9(hono@4.11.4) + '@hono/node-server': 1.19.9(hono@4.11.7) '@types/bun': 1.3.6 '@types/ws': 8.18.1 ws: 8.19.0 @@ -6696,9 +6685,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@hono/node-server@1.19.9(hono@4.11.4)': + '@hono/node-server@1.19.9(hono@4.11.7)': dependencies: - hono: 4.11.4 + hono: 4.11.7 optional: true '@huggingface/jinja@0.5.3': @@ -9098,84 +9087,6 @@ snapshots: dependencies: clsx: 2.1.1 - clawdbot@2026.1.24-3(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3): - dependencies: - '@agentclientprotocol/sdk': 0.13.1(zod@4.3.6) - '@aws-sdk/client-bedrock': 3.975.0 - '@buape/carbon': 0.14.0(hono@4.11.4) - '@clack/prompts': 0.11.0 - '@grammyjs/runner': 2.0.3(grammy@1.39.3) - '@grammyjs/transformer-throttler': 1.2.1(grammy@1.39.3) - '@homebridge/ciao': 1.3.4 - '@line/bot-sdk': 10.6.0 - '@lydell/node-pty': 1.2.0-beta.3 - '@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6) - '@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6) - '@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6) - '@mariozechner/pi-tui': 0.49.3 - '@mozilla/readability': 0.6.0 - '@sinclair/typebox': 0.34.47 - '@slack/bolt': 4.6.0(@types/express@5.0.6) - '@slack/web-api': 7.13.0 - '@whiskeysockets/baileys': 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5) - ajv: 8.17.1 - body-parser: 2.2.2 - chalk: 5.6.2 - chokidar: 5.0.0 - chromium-bidi: 13.0.1(devtools-protocol@0.0.1561482) - cli-highlight: 2.1.11 - commander: 14.0.2 - croner: 9.1.0 - detect-libc: 2.1.2 - discord-api-types: 0.38.37 - dotenv: 17.2.3 - express: 5.2.1 - file-type: 21.3.0 - grammy: 1.39.3 - hono: 4.11.4 - jiti: 2.6.1 - json5: 2.2.3 - jszip: 3.10.1 - linkedom: 0.18.12 - long: 5.3.2 - markdown-it: 14.1.0 - node-edge-tts: 1.2.9 - osc-progress: 0.3.0 - pdfjs-dist: 5.4.530 - playwright-core: 1.58.0 - proper-lockfile: 4.1.2 - qrcode-terminal: 0.12.0 - sharp: 0.34.5 - sqlite-vec: 0.1.7-alpha.2 - tar: 7.5.4 - tslog: 4.10.2 - undici: 7.19.0 - ws: 8.19.0 - yaml: 2.8.2 - zod: 4.3.6 - optionalDependencies: - '@napi-rs/canvas': 0.1.88 - node-llama-cpp: 3.15.0(typescript@5.9.3) - transitivePeerDependencies: - - '@discordjs/opus' - - '@modelcontextprotocol/sdk' - - '@types/express' - - audio-decode - - aws-crt - - bufferutil - - canvas - - debug - - devtools-protocol - - encoding - - ffmpeg-static - - jimp - - link-preview-js - - node-opus - - opusscript - - supports-color - - typescript - - utf-8-validate - cli-cursor@5.0.0: dependencies: restore-cursor: 5.1.0 @@ -9217,7 +9128,7 @@ snapshots: npmlog: 6.0.2 rc: 1.2.8 semver: 7.7.3 - tar: 7.5.4 + tar: 7.5.7 url-join: 4.0.1 which: 2.0.2 yargs: 17.7.2 @@ -9650,12 +9561,6 @@ snapshots: forever-agent@0.6.1: {} - form-data@2.3.3: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - form-data@2.5.5: dependencies: asynckit: 0.4.0 @@ -9851,7 +9756,7 @@ snapshots: highlight.js@11.11.1: {} - hono@4.11.4: {} + hono@4.11.7: {} hookified@1.15.0: {} @@ -10930,8 +10835,6 @@ snapshots: dependencies: side-channel: 1.1.0 - qs@6.5.3: {} - queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -11035,7 +10938,7 @@ snapshots: combined-stream: 1.0.8 extend: 3.0.2 forever-agent: 0.6.1 - form-data: 2.3.3 + form-data: 2.5.5 har-validator: 5.1.5 http-signature: 1.2.0 is-typedarray: 1.0.0 @@ -11044,7 +10947,7 @@ snapshots: mime-types: 2.1.35 oauth-sign: 0.9.0 performance-now: 2.1.0 - qs: 6.5.3 + qs: 6.14.1 safe-buffer: 5.2.1 tough-cookie: 2.5.0 tunnel-agent: 0.6.0 @@ -11470,7 +11373,7 @@ snapshots: tailwindcss@4.1.17: {} - tar@7.5.4: + tar@7.5.7: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 diff --git a/src/gateway/auth.ts b/src/gateway/auth.ts index 1adc367a2..4f9bf321c 100644 --- a/src/gateway/auth.ts +++ b/src/gateway/auth.ts @@ -1,7 +1,7 @@ -import { timingSafeEqual } from "node:crypto"; import type { IncomingMessage } from "node:http"; import type { GatewayAuthConfig, GatewayTailscaleMode } from "../config/config.js"; import { readTailscaleWhoisIdentity, type TailscaleWhoisIdentity } from "../infra/tailscale.js"; +import { safeEqual } from "../security/safe-equal.js"; import { isTrustedProxyAddress, parseForwardedForClientIp, resolveGatewayClientIp } from "./net.js"; export type ResolvedGatewayAuthMode = "token" | "password"; @@ -32,11 +32,6 @@ type TailscaleUser = { type TailscaleWhoisLookup = (ip: string) => Promise; -function safeEqual(a: string, b: string): boolean { - if (a.length !== b.length) return false; - return timingSafeEqual(Buffer.from(a), Buffer.from(b)); -} - function normalizeLogin(login: string): string { return login.trim().toLowerCase(); } diff --git a/src/gateway/server-http.ts b/src/gateway/server-http.ts index f08dc811c..6ea3168c4 100644 --- a/src/gateway/server-http.ts +++ b/src/gateway/server-http.ts @@ -14,6 +14,7 @@ import type { createSubsystemLogger } from "../logging/subsystem.js"; import { handleSlackHttpRequest } from "../slack/http/index.js"; import { resolveAgentAvatar } from "../agents/identity-avatar.js"; import { handleControlUiAvatarRequest, handleControlUiHttpRequest } from "./control-ui.js"; +import { safeEqual } from "../security/safe-equal.js"; import { extractHookToken, getHookChannelError, @@ -77,7 +78,7 @@ export function createHooksRequestHandler( } const { token, fromQuery } = extractHookToken(req, url); - if (!token || token !== hooksConfig.token) { + if (!token || !hooksConfig.token || !safeEqual(token, hooksConfig.token)) { res.statusCode = 401; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end("Unauthorized"); diff --git a/src/infra/node-pairing.ts b/src/infra/node-pairing.ts index f852ff420..54ae20fbe 100644 --- a/src/infra/node-pairing.ts +++ b/src/infra/node-pairing.ts @@ -1,7 +1,9 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; + import { resolveStateDir } from "../config/paths.js"; +import { safeEqual } from "../security/safe-equal.js"; export type NodePairingPendingRequest = { requestId: string; @@ -268,7 +270,7 @@ export async function verifyNodeToken( const normalized = normalizeNodeId(nodeId); const node = state.pairedByNodeId[normalized]; if (!node) return { ok: false }; - return node.token === token ? { ok: true, node } : { ok: false }; + return safeEqual(node.token, token) ? { ok: true, node } : { ok: false }; } export async function updatePairedNodeMetadata( diff --git a/src/security/audit-extra.ts b/src/security/audit-extra.ts index 3a92a30a8..6effc06c2 100644 --- a/src/security/audit-extra.ts +++ b/src/security/audit-extra.ts @@ -9,6 +9,7 @@ import { resolveNativeSkillsEnabled } from "../config/commands.js"; import { resolveOAuthDir } from "../config/paths.js"; import { formatCliCommand } from "../cli/command-format.js"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; +import { safeEqual } from "./safe-equal.js"; import type { AgentToolsConfig } from "../config/types.tools.js"; import { resolveBrowserConfig } from "../browser/config.js"; import { isToolAllowedByPolicies } from "../agents/pi-tools.policy.js"; @@ -181,7 +182,7 @@ export function collectHooksHardeningFindings(cfg: MoltbotConfig): SecurityAudit gatewayAuth.token.trim() ? gatewayAuth.token.trim() : null; - if (token && gatewayToken && token === gatewayToken) { + if (token && gatewayToken && safeEqual(token, gatewayToken)) { findings.push({ checkId: "hooks.token_reuse_gateway_token", severity: "warn", diff --git a/src/security/safe-equal.ts b/src/security/safe-equal.ts new file mode 100644 index 000000000..6805a490b --- /dev/null +++ b/src/security/safe-equal.ts @@ -0,0 +1,10 @@ +import { timingSafeEqual } from "node:crypto"; + +/** + * Timing-safe string comparison. Use this for any secret/token comparison + * to prevent timing attacks. Returns false if either string is empty. + */ +export function safeEqual(a: string, b: string): boolean { + if (a.length !== b.length) return false; + return timingSafeEqual(Buffer.from(a), Buffer.from(b)); +}