Skip to content

fix(security): close findings from the canary security audit - #913

Merged
aXenDeveloper merged 1 commit into
canaryfrom
claude/canary-security-fixes
Nov 5, 2026
Merged

aXenDeveloper merged 1 commit into
canaryfrom
claude/canary-security-fixes

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Improving Documentation

  • pnpm lint:fix and pnpm lint pass on the branch.
  • The docs pages that describe changed behaviour are updated in the same commits.

Description

What?

These are fixes from a multi-pass security review of @vitnode/core and the shipped plugins.

High

  • Rich text: a stored table colspan of 30,000,000 used up all the server-side renderer's memory and killed the process on every view. A member could cause this with one 187-byte post. Spans are now capped when rendering, and values above HTML's limits are rejected when saved.
  • Content files: a member could reference another user's fileId in rich text and download it through the protected file route. Rich-text file ids must now be the writer's own uploads or files already on the record. The download route also checks that the uploader is the record owner or staff.

Medium

  • Sign-in and sessions
    • Password sign-in re-reads the password hash after writing the session. A sign-in racing a password reset can no longer keep a session.
    • Adding a passkey needs a sign-in from the last hour (reauthentication_required). A stolen cookie can no longer plant a key that survives a password reset.
    • SSO: only emailVerified === true is trusted. An omitted value fails closed.
  • Payments: resuming a checkout that already succeeded throws already_paid instead of starting a second charge.
  • Content and comments
    • Soft-deleted comments: no longer indexed as public, editable or repliable.
    • An update re-checks access for the new values, such as a move into a restricted category.
    • field.file(): a match on content type is honoured for staff only. "Protect files" skips avatars, covers and other content types' files.
    • Unpublished translation titles no longer show in search contexts.
  • Caching: public read cache generations are pinned per request (pinContentPublicGeneration), so an access-rule change mid-request can't be cached for guests.
  • WebSockets: live-editing and thread-refresh re-authorization drop memoised access and staff state, so a demotion takes effect within a minute. Sign-out closes the device's sockets.
  • AI: a run the caller cancels now charges points and the daily count. Dropping the connection is no longer a free way to spend the site budget. The docs are updated, and this is easy to revert if you prefer the old rule.
  • Other medium fixes
    • The member timeline no longer skips visibility checks on your own profile.
    • Time zones are stored and cached by their canonical name. The cache grew with every capitalisation sent.

Low and hardening

  • Redirects and links
    • Open redirects: /.//host is refused after parsing in notification targets and sanitizeReturnTo.
    • Header search refuses backslash paths; useHrefNavigate only leaves the app for http(s) and mailto.
    • Report target links are limited to site paths or https; AdminCP update links require https.
    • The editor's attachment href scheme is re-checked.
  • Existence oracles
    • Popularity verifies hits before recording; the search keyset cursor reads through the viewer's filters.
    • Delete and hide return 404 for drafts and hidden records.
  • Races
    • The report hourly cap and the verification-email resend cooldown are checked under locks.
    • A staff-permission cache fill can no longer write back a revoked permission.
  • Caching and headers
    • /thread/branches is no-store unless the access model is shareable.
    • The page cache skips renders that read a 429 or 5xx.
    • The shared-render header carries a per-process token.
    • Delivery routes send Vary: Accept-Language.
  • Rate limits and caps
    • Admin password sign-in answers a non-staff account like a wrong password.
    • SSO link and pending-email routes use the auth rate limit.
    • Search inputs are capped, and popularity dedupe keys guests by /64.
    • Live documents are capped at 16 MB.
  • Permissions and boot checks
    • The admin notification send needs notifications:can_manage.
    • allowedScopes denies an unknown family, and access.from pointing at a non-scope type fails at boot.
  • Storage and exports
    • Storage only stores known Content-Types; anything else becomes application/octet-stream.
    • The test-AI output is capped, and the CSV formula guard covers leading spaces and full-width characters.
  • Dependencies: zod 4.6.7 and sanitize-html 2.18.1, which fix published advisories.

Tests: fixtures and expectations changed only where behaviour changed on purpose:

  • SSO fixtures set emailVerified: true;
  • the passkey harness starts with a fresh session;
  • the own-timeline, staff-cache and sign-in select-count tests match the new rules.

Why?

Each item was traced from a realistic request to its impact before it was fixed. Regression tests for the new behaviour should come in a follow-up PR (repo rule: tests in a separate session).

Left for a maintainer decision (not in this PR)

  • CLI and secrets
    • vitnode migrate generates and applies migrations in production without asking.
    • The raw CRON_SECRET is sent to content.revalidateOrigins.
  • Data exposure
    • Sign-up's 409 reveals registered emails.
    • Reset links stay in queue payloads for 7 days.
    • EXIF is kept without storage.image.
  • Resource limits
    • WebSocket sockets and joins have no caps.
    • Uploads have no quota, and the per-role storage limit isn't enforced.
  • Permissions and moderation
    • /admin/ai/assist has no specific permission.
    • The relation picker shows titles across content types.
    • Report queue scopes go stale after a move.
    • Live editors can spoof each other's presence.
  • Hardening
    • __Host- cookie prefix, CSP and HSTS.
    • The plugin cache namespace in background code.

Generated by Claude Code

@github-actions github-actions Bot added the 🐞 Bug Something isn't working label Nov 4, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T15:09:00.594443Z 59a65c4 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

React Doctor found 2 new issues in 1 file · 2 warnings · score 87 / 100 (Great) · 0 fixed · vs canary

2 warnings

src/content/server/public-write-service.ts

  • ⚠️ L382 await inside a loop async-await-in-loop
  • ⚠️ L395 await inside a loop async-await-in-loop

Reviewed by React Doctor for commit 59a65c4. See inline comments for fixes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa0490f0e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +165 to +169
!byAuthor &&
file.userId !== null &&
((await isStaff(c, { type: "admin", userId: file.userId })) ||
(await isStaff(c, { type: "moderator", userId: file.userId })));
if (!byAuthor && !byStaff) return missing();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve attachment access after the uploader leaves staff

When a moderator uploads an attachment while editing another member's post, this check makes the attachment's availability depend on the uploader remaining staff forever. Removing their staff entry through admin/staff/routes/delete.route.ts makes both checks false, so existing protected images and attachments start returning 404 even though the post, file reference, and reader's access are unchanged. Deleting an uploader's account also breaks this path because core_files.userId uses onDelete: "set null". Validate and persist the attachment's association when it is added rather than requiring the uploader's current identity or staff status on every download.

Useful? React with 👍 / 👎.

- Content files, comments, SSO and payments audit findings
- Pin cache generation per request, re-auth for passkeys
- HTTPS-only update links in the AdminCP, wider CSV formula guard
- Findings from the full, line-by-line and thematic core scans
- Staff and user routes invalidate all staff permissions
- Align passkey and timeline tests with the new security rules

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aXenDeveloper
aXenDeveloper force-pushed the claude/canary-security-fixes branch from 06446aa to 59a65c4 Compare November 5, 2026 15:06

const owned = new Set(
(
await tx

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

if (owned.has(fileId)) continue;
if (
itemId !== undefined &&
(await contentRecordHasFile(c, accessModel, itemId, fileId))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/async-await-in-loop (warning)

This for…of loop waits before starting the next iteration. If iterations perform independent asynchronous work, consider bounded concurrency; await syntax alone does not establish a speedup.

Fix → Consider concurrent calls only for independent asynchronous work. Shared queues or synchronous work may not benefit. Preserve resource limits, transaction ordering, and failure/cancellation semantics; observe all callback promises.

Docs

@aXenDeveloper
aXenDeveloper merged commit 4b1c4f6 into canary Nov 5, 2026
7 checks passed
@aXenDeveloper
aXenDeveloper deleted the claude/canary-security-fixes branch November 5, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant