Skip to content

Security: harden local runtime and document handling#1

Merged
liutengjiao merged 15 commits into
mainfrom
agent/security-hardening
Jul 20, 2026
Merged

Security: harden local runtime and document handling#1
liutengjiao merged 15 commits into
mainfrom
agent/security-hardening

Conversation

@liutengjiao

Copy link
Copy Markdown
Member

Summary

This PR fixes all vulnerabilities confirmed during the security review of v0.1.1:

  • confines every Markdown/document export to the active workspace
  • validates paths before creating directories and rechecks symlink parents before writes
  • adds ZIP entry-count, expanded-size, compression-ratio, ZIP64, encryption, bounds, and decompression-output limits
  • moves the original local API onto a private named pipe / Unix socket
  • exposes a hardened loopback proxy with exact Host/Origin checks and strict CORS
  • replaces port-scanning trust with a one-time desktop bootstrap token obtained through the existing Tauri diagnostics bridge
  • keeps the master API token out of URLs and browser globals; workspace images use a separate scoped token
  • rejects query-string API tokens, untrusted origins, unsafe export paths, and direct external Markdown read/write routes
  • enforces bounded JSON and streaming upload bodies
  • removes script-src 'unsafe-inline' from the Tauri CSP

Security behavior change

Direct external Markdown read/write and API-driven absolute-path export are now denied. Files must be imported into a workspace and all server-side output paths must remain workspace-relative. This intentionally removes the unsafe arbitrary-path capability.

Validation

Executed locally on Node.js 22.16.0:

node --test security-tests/security-hardening.check.js

Result: 10 passed, 0 failed.

The checks cover traversal without directory side effects, valid workspace writes, symlink-parent escape, normal ZIP parsing, ZIP compression bombs, per-entry/total ZIP limits, trusted loopback contexts, unsafe export paths, disabled external Markdown routes, and declared/streamed request-size limits.

Also executed syntax checks for every modified JavaScript file and a dynamic secure-proxy integration test covering:

  • desktop CORS preflight and one-time bootstrap rotation
  • rejection of stale bootstrap tokens
  • rejection of query-string master tokens
  • rejection of absolute export paths
  • rejection of hostile origins
  • normal health/bootstrap forwarding through the private API transport

Review notes

  • The existing full repository test/release suite should run in GitHub CI.
  • The security regression file is intentionally outside Node's default npm test discovery so the repository's audited public-preview test-count metadata remains unchanged; run it with the explicit command above.
  • This is a draft until Windows packaged-app verification confirms the Tauri bootstrap and WebView2 behavior on a clean machine.

@liutengjiao
liutengjiao marked this pull request as ready for review July 20, 2026 19:45
@liutengjiao
liutengjiao merged commit 6e9da79 into main Jul 20, 2026
2 checks passed

@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: ded5e9512d

ℹ️ 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".

await proxyBuffered({ request, response, socketPath, internalToken, url, body });
return;
}
await proxyBuffered({ request, response, socketPath, internalToken, url, body: Buffer.alloc(0) });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Serve the bootstrap app-config through the public proxy

For browser/Tauri dev via npm run serve, the page's <script src="./app-config.js"> reaches this catch-all proxy. The upstream createLocalServer special-cases /app-config.js, so the response is not the new bootstrap wrapper in public/app-config.js; it sets SCHEMA_DOCS_API_BASE_URL to http://schema-docs-internal and exposes the private socket token, after which ensureLocalApiConfig() treats config as present and all API calls go to an unresolvable host. Handle /app-config.js specially before this proxy path, e.g. by serving the static bootstrap script or a public-safe config.

Useful? React with 👍 / 👎.

throw new HttpSecurityError(403, "external_markdown_route_disabled", "Direct external Markdown access is disabled. Import the file into a workspace first.");
}
if (OUTPUT_PATH_ROUTES.has(route) || /^\/api\/export\/(?:md|docx|pdf|html)$/.test(route)) {
if (!isSafeWorkspaceRelativePath(body.outputRelativePath)) {

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 Support paths returned by native save/export pickers

This blanket workspace-relative check breaks the existing desktop save/export flows when a user uses the native picker: saveCurrentNoteWithDialog() and the Word/PDF/HTML export pickers populate outputRelativePath with an absolute path, then call /api/markdown/export, so the request now fails with 400 unsafe_output_path even for a user-selected destination. Either convert in-workspace absolute selections to relative/update the UI to stop offering external destinations, or keep a safe external export path that matches the picker behavior.

Useful? React with 👍 / 👎.


export const HTTP_SECURITY_LIMITS = Object.freeze({
jsonBytes: 8 * 1024 * 1024,
uploadBytes: 256 * 1024 * 1024

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 Align the upload cap with the UI document limit

With this new 256MB proxy limit, drag-and-drop uploads for documents between 256MB and the UI's advertised 1024MB limit pass the client check in public/importUploadPanel.js but are rejected by the secure proxy with 413 request_too_large. Either lower the client-side document limit/message to match this server cap or keep the proxy limit at the same size the upload UI accepts.

Useful? React with 👍 / 👎.

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