Skip to content

Audit remediation: security hardening, power-user options, packaging & docs#2

Merged
PeterM45 merged 14 commits into
mainfrom
fix/audit-0.1.2
Jun 9, 2026
Merged

Audit remediation: security hardening, power-user options, packaging & docs#2
PeterM45 merged 14 commits into
mainfrom
fix/audit-0.1.2

Conversation

@PeterM45

@PeterM45 PeterM45 commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Implements the fixes from the two multi-agent audits of the package and codebase. Recommend releasing as 0.2.0, not 0.1.2 — two deliberate breaking behavior changes (below). Local gate is fully green: strict typecheck (with exactOptionalPropertyTypes), Biome, 98 tests (was 43), coverage 95.0% stmts / 85.1% branch, dist smoke test, publint clean, and attw reporting 0 problems across all 11 entry points.

Breaking changes (why 0.2.0)

  • Existing sessions invalidate once on upgrade. Tokens now sign a digest of the password, so rotating SITEPASS_PASSWORD actually revokes outstanding sessions. Existing cookies force one re-login.
  • Secrets shorter than 16 characters now fail closed (503) instead of signing tokens with a brute-forceable key. Anything sitepass init wrote is unaffected.

Security

  • publicPaths: reject literal and encoded (%5c, %25, %2e, %2f) and path-parameter (;) traversal; / is an exact root match (was un-gating the whole site); the login POST is handled before publicPaths (an entry covering loginPath no longer breaks login). The encoded-only case was the 0.1.1 fix; this closes the literal/proxy/Express route.
  • Reverse proxy: strips the gate cookie and the x-sitepass-bypass credential before forwarding; sets X-Forwarded-For/-Proto/-Host authoritatively (client-spoofed values stripped); caps the unauthenticated login body at 64 KiB.
  • Login body capped at 64 KiB on every adapter (Bun/Hono were previously unbounded).

Features (power-user QoL)

  • Bypass token for CI/E2E/uptime monitors (SITEPASS_BYPASS_TOKEN / bypassToken + x-sitepass-bypass header, constant-time).
  • Logout at <loginPath>/logout (GET/POST), renderLoginPage hook (custom/localized pages, escapeHtml exported), onAuthFailure hook, cookieSecure: false for plain-HTTP LAN.
  • sitepass/proxy is now an importable export; the proxy CLI gained --public-paths, --login-path, --cookie-name, --session-seconds, --bypass-token, --insecure-cookie, --env-file.
  • CLI: --help/--version behave (exit 0), init --help prints usage, strict unknown-flag rejection, dependency-free .env loader (no longer silently no-ops on Node 20.0–20.11).

Types & packaging

  • PagesFunction<Env> = gate() now compiles (verified against @cloudflare/workers-types); Bun adapter is generic over the handler's rest args (server for websocket upgrades); Netlify fails closed instead of throwing outside the Edge runtime.
  • Every adapter exports its options + context types; publicPaths accepts readonly string[]; typesVersions map fixes node10 type resolution; sideEffects: false; @types/express optional peer; ./proxy export; CHANGELOG.md ships in the tarball.

Internals & CI

  • All 7 web adapters delegate to a shared internal src/web.ts; Node body readers shared via src/node-body.ts.
  • CI: dist smoke test + publint before merge and publish; tag-vs-version publish guard; concurrency groups; job timeouts; pinned Node for coverage; Dependabot; issue/PR templates.

Manual follow-ups (not in this PR)

  1. Enable GitHub private vulnerability reporting (Settings → Code security) — SECURITY.md points there, with an email fallback added.
  2. Bump package.json to 0.2.0 and tag at release time (the publish workflow enforces tag == version).

Full details in the CHANGELOG [Unreleased] section.

Summary by CodeRabbit

  • New Features

    • Added bypass tokens for CI/E2E testing
    • Added logout endpoint functionality
    • Added custom login page rendering support
    • Added auth failure observer hook
    • Added cookie security controls and request body size limits
  • Bug Fixes

    • Fixed session invalidation on password rotation
    • Hardened path traversal and header-injection protections
  • Documentation

    • Enhanced README with setup guidance and reverse proxy details
    • Updated contributing and security documentation
  • Chores

    • Updated GitHub workflows and build infrastructure

PeterM45 added 13 commits June 9, 2026 14:23
…ry footguns

- reject literal dot-segments and backslash segments before prefix
  matching: the reverse proxy and Express adapters feed the raw request
  target into the gate, so /assets/../secret matched an /assets prefix
  verbatim while resolving elsewhere at the origin (the encoded form was
  already rejected in 0.1.1)
- treat a '/' entry as an exact match on the root path: trailing-slash
  normalization left an empty base whose prefix check matched every path,
  silently un-gating the whole site; empty entries are now ignored
- handle the login POST before publicPaths matching, so an entry covering
  loginPath can no longer make login impossible
…handler, options) signature

The Targets table marked SvelteKit as gating static output, but hooks.server.ts
only runs for server-rendered requests: prerendered pages and /_app client
assets bypass it, the same boundary the README already documents for Astro.
The Configuration section also implied every adapter takes gate(options),
which mints a broken gate on Bun where the handler comes first.
…etadata

dist/proxy.* already shipped in the tarball but no './proxy' entry existed in
the exports map, so startProxy (which accepts the full gate options) was
unreachable dead weight. Also add sideEffects: false (publint's one
suggestion; every module is a pure factory) and ship CHANGELOG.md — 0.1.1 is
an 'upgrading recommended' security release consumers couldn't read from the
installed package.
…version match

Tests import src/, so the 10 dual-format entry points shipped without ever
being executed — a tsup or exports-map regression would publish green.
scripts/smoke-dist.mjs imports every dist entry in both formats (tolerating
only host-resolved specifiers: $env/dynamic/private, next/server), asserts
the expected export, and runs the CLI. The publish workflow now refuses a tag
that doesn't match package.json and builds + smokes the exact artifact before
npm publish; the trigger is tightened to v[0-9]*.
…oken, cookieSecure, hooks

- tokens now sign expiry + a password digest, so rotating SITEPASS_PASSWORD
  (the obvious lever when the shared password leaks) invalidates every
  outstanding session instead of leaving cookies valid for up to 7 days;
  existing sessions get one forced re-login on upgrade
- GET/POST {loginPath}/logout clears the cookie and redirects to /
- bypassToken option + x-sitepass-bypass header let CI, E2E runs, and uptime
  monitors through without scripting the login form (constant-time compare,
  same shape as the password check)
- cookieSecure: false escape hatch for plain-HTTP LAN deployments, which
  previously failed as a silent login loop (default unchanged: Secure on)
- secrets shorter than 16 chars now count as unconfigured and fail closed
  instead of minting tokens with a brute-forceable HMAC key
- onAuthFailure observer for fail2ban-style logging; renderLoginPage escape
  hatch for custom/localized login pages
- JSDoc on password/secret/brand, readonly publicPaths and Gate fields
- regression tests: malformed tokens, CR/LF next injection, all of the above
…everywhere, type-boundary fixes

All 7 web adapters now delegate to an internal src/web.ts (gateWebRequest /
toResponse), deleting the ~20-line GateRequest/GateResult translation that was
copied per adapter — and with it the per-adapter isLoginPost re-implementation.
The helper reads the login body incrementally with a 64 KiB cap, so bun and
hono (self-hosted runtimes with no platform cap) get the same 413 fail-closed
behavior express already had; every adapter now accepts maxBodyBytes. The
byte-identical body readers in proxy.ts/express.ts moved to src/node-body.ts.
Both internal modules are bundled into shared chunks, not dist entries.

Type-boundary fixes, verified against consumer annotations:
- cloudflare: env typed as named optional unknown fields instead of an index
  signature, so 'PagesFunction<Env>' annotations compile; non-string bindings
  count as unset (fail closed) instead of flowing into the gate
- bun: gate() is generic over the handler's rest args, so '(req, server)'
  websocket-upgrade handlers keep their signature and receive 'server'
- netlify: the Netlify global is typeof-guarded — outside the Edge runtime the
  gate fails closed (503) instead of throwing ReferenceError
- every adapter exports a named XxxGateOptions type and its context interface
- express: returns express's own RequestHandler type; reads x-sitepass-bypass

Tests: per-adapter blocks replaced by a conformance runner (each adapter
supplies an ~10-line driver); conformance now also asserts the 413 body cap,
logout, and bypass-token behavior on all 7 adapters; cloudflare gains
adapter-level fail-closed tests; shared credentials moved to test/fixtures.
… the forward path

The proxy's own session token no longer leaks to the origin (anything logging
request headers there would capture a replayable credential); other cookies
forward unchanged, and the Cookie header is dropped when the gate cookie was
the only one. The origin now receives X-Forwarded-For/-Proto/-Host, so its
logs, rate limiting, and absolute-URL generation see the real client. Also
passes the bypass header through to the gate and types handle() with the
exported Gate interface.

New forward-path tests: byte-for-byte body forwarding, host pinning +
hop-by-hop stripping + cookie stripping + X-Forwarded-* assertions via an
origin header echo, the unauthenticated login-body 413 cap, and a rewritten
mid-body reset test that actually exercises the streaming pipeline catch
(headers flushed and first chunk read before the upstream is destroyed).
…strict flags, unit tests

- 'sitepass --help' and '-h' print usage and exit 0 (previously 'Unknown
  command' + exit 1); '--version'/'-v' print the package version (injected by
  tsup at build time); 'sitepass init --help' prints usage instead of starting
  an interactive init
- the proxy accepts the gate options the README promises for every deployment
  mode: --public-paths, --login-path, --cookie-name, --session-seconds,
  --bypass-token (or SITEPASS_BYPASS_TOKEN), and --insecure-cookie for
  plain-HTTP LAN origins; --port is validated instead of silently defaulting
- unknown flags are a hard error naming the known set, so a typo'd
  --public-paths can never be silently ignored
- --env-file redirects both init's write target and the proxy's env source;
  .env loading no longer depends on process.loadEnvFile (Node >=20.12 only,
  silently absent on 20.0-20.11) — a dependency-free parser replaces it
- startup warns when either SITEPASS_PASSWORD or SITEPASS_SECRET is missing,
  naming the variable (previously only a missing secret warned)
- the entrypoint runs only when executed as a script (realpath-aware is-main
  guard), making the helpers importable: parseFlags, upsertEnv (including the
  0600-permission regression test for the 0.1.1 fix), ensureGitignored,
  readEnvValue, loadDotenv now have unit tests; upsertEnv no longer leaves a
  blank line when appending to a newline-terminated file
- every adapter reads SITEPASS_BYPASS_TOKEN from its environment, so the
  bypass token needs no code change to enable
- tsconfig gains exactOptionalPropertyTypes (optionals widened with
  '| undefined' so consumer assignments stay compatible); vitest alias uses
  fileURLToPath so the suite resolves on Windows checkouts; coverage floors
  raised to 90/79/93/93 to match actual coverage (94.8/84.5/97.5/97.3)
…dabot; issue/PR templates

- PR branches run CI once (pull_request) instead of twice (push duplicated
  every PR run, confirmed in the run history); superseded pushes cancel
  in-flight runs; every job gets timeout-minutes: 10 instead of GitHub's
  360-minute default
- the quality job pins Node 22 via setup-node — the coverage gate previously
  ran on whatever Node shipped with the rotating ubuntu-latest image, and v8
  coverage output shifts between Node majors
- dependabot keeps the SHA-pinned actions fresh (it updates pin + version
  comment together) and groups weekly dev-dependency bumps; without it the
  pins rot silently
- bug-report template asks the first triage question (which adapter?) up
  front and routes security reports to private reporting; PR template points
  at the CONTRIBUTING guardrails
- package.json: keywords now cover all shipped adapters (express, hono, bun)
  plus password-protection/staging; @types/express added as an optional peer
  so TS consumers learn dist/express.d.ts needs it instead of hitting TS2307
…ed changelog

README gains badges, a Netlify quickstart (the required config re-export was
documented nowhere user-facing), a CLI reference, a core-API section for
custom adapters, framework version requirements in the Targets table, and
sections for the bypass token, logout, renderLoginPage, and onAuthFailure.
The Configuration block now lists every real option including maxBodyBytes.
The localhost TLS guidance is corrected (wrangler pages dev serves plain HTTP;
Chrome/Firefox exempt localhost from the Secure requirement, Safari doesn't).

SECURITY.md gains an email fallback for the reporting channel and a rotation
section documenting that password or secret rotation revokes all sessions.
CONTRIBUTING's adapter contract reflects the shared web/node-body plumbing
and the conformance-driver test pattern. CHANGELOG documents everything on
this branch including the two deliberate behavior changes, and 0.1.0 gets
its date and link. escapeHtml is exported for renderLoginPage implementers.
…ode10 resolution

attw reported 'node10: resolution failed' for every subpath: consumers on
TypeScript <= 5.x with moduleResolution 'node' (still the default for plain
module:commonjs tsconfigs there) got TS2307 for sitepass/express et al, even
though Node itself resolves them via the exports map at runtime. attw is now
fully green across node10/node16-cjs/node16-esm/bundler for all 11 entries.
Security:
- isPublicPath: reject encoded backslash (%5c), stray/double percent (%25),
  and path-parameter (';') segments too — the prior pass caught literal
  ../backslash and %2e/%2f but missed /assets/..%5c..%5csecret and double
  encoding. Broadened the encoded-traversal regex and the literal-segment scan.
- proxy: X-Forwarded-For/-Proto/-Host are now set authoritatively — inbound
  client values are stripped first, so a client can no longer spoof its IP or
  inject x-forwarded-host (cache poisoning / host-header injection) to the
  origin. The previous version appended to client XFF and deferred proto/host
  to the client, which for a front-most gate proxy is exactly backwards.
- proxy: the x-sitepass-bypass credential is stripped before forwarding,
  alongside the gate cookie — it was leaking to origin logs.
- proxy: the login POST body is capped at 64 KiB independently of the (10 MiB)
  forward-body limit, so an unauthenticated login POST can't buffer to 10 MiB.

Correctness:
- core: a NaN/Infinity sessionSeconds falls back to the default instead of
  minting a 'NaN'-expiry token that never validates (silent login loop); the
  CLI rejects a non-numeric/non-positive --session-seconds like it does --port.
- cloudflare: the PagesContext.env fix was still a weak type (all-optional),
  so 'PagesFunction<Env> = gate()' still failed for a bindings-only Env. env is
  now typed 'object' (any binding interface satisfies it) and cast internally;
  verified compiling against @cloudflare/workers-types.
- core: logout responds to GET/POST only, so it no longer shadows a consumer
  route on other verbs and narrows the forced-logout surface.
- cli: 'sitepass init' warns when it keeps an existing sub-16-char secret
  (which now fails closed); --insecure-cookie=true is honored (was silently
  ignored — only the bare flag worked).
- proxy: ProxyOptions.maxBodyBytes gains '| undefined' to match the rest of the
  surface under exactOptionalPropertyTypes.

Polish:
- module-header comments on core.ts/proxy.ts converted to plain // so the
  emitted d.ts attaches docs to createGate/startProxy, not GateOptions/ProxyOptions.
- docs: publint now runs in publish.yml (not just CI); CONTRIBUTING's adapter
  snippet has the required await; CHANGELOG corrected (proxy 64 KiB login cap,
  typesVersions bullet, authoritative X-Forwarded) and flags this as a 0.2.0
  release; CONTRIBUTING build line notes the CLI is ESM-only.

Tests: encoded-backslash/double-encoding/path-param traversal, logout method
restriction, NaN sessionSeconds fallback, bypass-header stripping, X-Forwarded
spoof rejection, flagEnabled. 98 tests; coverage 95.0/85.1/97.5/97.3.
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PeterM45, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 37 minutes and 34 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 81903343-53cb-4260-8e42-e00a08cfa657

📥 Commits

Reviewing files that changed from the base of the PR and between 2f8a9cd and df563b5.

📒 Files selected for processing (12)
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • .github/workflows/ci.yml
  • .github/workflows/publish.yml
  • CHANGELOG.md
  • README.md
  • src/cli.ts
  • src/core.ts
  • src/node-body.ts
  • src/proxy.ts
  • test/core.test.ts
  • test/node-body.test.ts
  • test/proxy.test.ts
📝 Walkthrough

Walkthrough

This PR introduces significant hardening to the sitepass authentication library. The core API now supports bypass tokens, session logout, password rotation invalidation via token-embedded password tags, auth failure observers, custom login page rendering, and stricter path traversal validation. All framework adapters are refactored to delegate request handling to a shared gateWebRequest utility, reducing duplication. The CLI is comprehensively refactored with built-in version awareness, flag validation, and a portable .env parser. Comprehensive test coverage and documentation updates accompany the feature additions.

Changes

Sitepass v0.1 hardening and adapter consolidation

Layer / File(s) Summary
Core API expansion
src/core.ts
Extends GateOptions with bypass tokens, cookie security control, auth failure observers, and login page renderers; session tokens now embed password-derived tags for rotation-based invalidation; adds logout endpoint; hardens public path traversal rejection with encoded-segment filtering.
Shared web adapter plumbing
src/web.ts
New module exports gateWebRequest to centralize request handling (capped body reading, fail-closed 413 responses, result-to-response conversion) across all adapters; also exports envString and toResponse utilities.
Node request body utilities
src/node-body.ts
New module exports BodyTooLargeError, readRawBody (UTF-8 text streaming), and readRawBuffer (binary streaming) with byte-limit enforcement and pause-on-exceed semantics for bounded memory usage.
Framework adapter refactoring
src/astro.ts, src/bun.ts, src/cloudflare.ts, src/express.ts, src/hono.ts, src/netlify.ts, src/next.ts, src/sveltekit.ts
All adapters refactored to delegate request handling to gateWebRequest instead of inline gate logic; each adds typed *GateOptions with optional maxBodyBytes; removes per-adapter result-type switching and response construction.
Proxy implementation and hardening
src/proxy.ts
Enhanced reverse proxy uses createGate and gateWebRequest for request gating; adds x-sitepass-bypass header extraction and constant-time bypass validation; sanitizes outgoing headers (strips hop-by-hop, proxy-owned, client-controlled); re-derives X-Forwarded-* headers authoritatively; introduces stripCookie helper to remove gate cookie from upstream requests.
CLI major refactor with validation and env loading
src/cli.ts
Refactors entry flow with __SITEPASS_VERSION__ build-time injection; adds explicit flag contracts and rejectUnknownFlags validation; exports parseFlags, flagEnabled for testing; rewrites init to warn on short secrets and reuse existing env vars; enhances proxy with port/session-seconds/public-paths validation and new --bypass-token/--insecure-cookie flags; implements portable .env parser via loadDotenv.
Core gate test expansion
test/core.test.ts, test/fixtures/credentials.ts
Extends coverage for password rotation, logout, bypass tokens, cookie security, short secrets, custom login renderers, auth failure observers, and malformed token rejection; adds public path traversal hardening tests.
Adapter conformance test harness
test/adapters.test.ts
Centralizes authentication and feature conformance testing via describeAdapterConformance helper; verifies 401 without session, pass-through with valid cookie, cookie minting on login, 413 on oversized body, logout clearing, and bypass token behavior across all adapters.
CLI helper function tests
test/cli.test.ts
New Vitest suite covering parseFlags, flagEnabled, rejectUnknownFlags, upsertEnv, readEnvValue, ensureGitignored, and loadDotenv.
Proxy robustness and conformance tests
test/proxy.test.ts, test/cloudflare.test.ts
Refactored upstream-reset test to verify mid-body resilience; adds 413 rejection test for oversized login bodies; expands forwarding conformance to verify host pinning, header stripping, cookie isolation, and authoritative X-Forwarded-\* headers; adds Cloudflare fail-closed and non-string env binding tests.
Build configuration refactoring
tsup.config.ts, tsconfig.json, vitest.config.ts, scripts/smoke-dist.mjs
Refactors tsup to multi-config array with explicit public entries and CLI-specific build; adds exactOptionalPropertyTypes to tsconfig; fixes SvelteKit alias resolution in vitest; adds new distribution smoke test script.
Package manifest and GitHub templates
package.json, .github/
Updates package.json with new keywords, adds CHANGELOG.md to published files, expands typesVersions/exports for framework targets; adds bug report issue template, PR template with checklist, Dependabot v2 config for GitHub Actions and Bun.
Documentation: README, CONTRIBUTING, SECURITY, CHANGELOG
README.md, CONTRIBUTING.md, SECURITY.md, CHANGELOG.md
Comprehensive updates including badges, secret minimum clarification, Netlify Edge setup, reverse proxy configuration, new CLI flags, custom login rendering, auth failure logging; expanded adapter contract; credential rotation guidance; comprehensive unreleased feature documentation.
CI/publish workflow enhancements
.github/workflows/ci.yml, .github/workflows/publish.yml
Scopes triggers to main branch and version tags; adds concurrency cancellation; extends quality job with smoke test and publint; adds job timeouts and pre-publish verification steps.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A gateway stands stronger with tokens to pass,
Sessions now remember when passwords transform,
Every adapter now speaks with a unified voice,
Through gateWebRequest they route and rejoice,
The CLI builds sharp, the tests prove it's all true! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main focus of the changeset: comprehensive audit remediation addressing security hardening, new power-user options, packaging improvements, and documentation updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/audit-0.1.2

@coderabbitai coderabbitai 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.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/publish.yml (1)

22-24: ⚠️ Potential issue | 🟠 Major

Harden publish workflow by disabling checkout credential persistence and Bun executable caching.

  • actions/checkout@v4 defaults persist-credentials: true, persisting auth material in the runner’s git config for the job.
  • oven-sh/setup-bun@v2 defaults no-cache: false, enabling caching/reuse of the downloaded Bun executable across workflow runs.
Suggested patch
-      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
-      - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
+      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+        with:
+          persist-credentials: false
+      - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
+        with:
+          no-cache: true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 22 - 24, Update the workflow
steps that call actions/checkout@v4 and oven-sh/setup-bun@v2: for the
actions/checkout step (identified by uses: actions/checkout@v4) add with:
persist-credentials: false to prevent persisting runner git credentials, and for
the oven-sh/setup-bun step (uses: oven-sh/setup-bun@v2) add with: no-cache: true
to disable reuse/caching of the Bun executable across runs; leave other steps
(e.g., actions/setup-node) unchanged.
src/express.ts (1)

47-55: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close the connection after rejecting an oversized login body.

readRawBody() pauses the request once the cap is hit. Sending a plain 413 here without Connection: close or an explicit socket teardown leaves unread bytes on a keep-alive connection, so a client can keep that socket occupied by continuing the oversized upload. Mirror the proxy path and close the connection after flushing the 413.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/express.ts` around lines 47 - 55, In the BodyTooLargeError branch inside
the isLoginPost handling, ensure the connection is closed after sending the 413:
set the response Connection: close header (e.g., via res.set('Connection',
'close') or res.setHeader) before calling
res.status(413).type('text/plain').send('Payload too large'), and teardown the
socket once the response is flushed (attach a finish listener to destroy the
request/response socket, e.g., res.on('finish', () => req.socket.destroy())).
This change should be applied around the readRawBody error handling where
BodyTooLargeError is caught so unread bytes can't occupy a keep-alive
connection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/ISSUE_TEMPLATE/bug_report.yml:
- Around line 38-42: Update the SECURITY.md link in the issue template so it
resolves correctly from .github/ISSUE_TEMPLATE: replace the current
../blob/main/SECURITY.md with a working path (either the repository-root
relative ../../SECURITY.md or an absolute URL like
https://github.com/PeterM45/sitepass/blob/main/SECURITY.md) in the markdown
block that starts with "**Security issues:** please do not file them here — use
the private reporting channel described in [SECURITY.md]"; after changing the
link, create a test issue using the template to verify the link opens the
SECURITY.md page.

In @.github/workflows/ci.yml:
- Around line 28-29: Update the GitHub Actions steps that use actions/checkout
and oven-sh/setup-bun: for the actions/checkout steps (identified by uses:
actions/checkout@...), add a with block setting persist-credentials: false to
avoid leaving auth/SSH credentials on the runner; for the oven-sh/setup-bun
steps (identified by uses: oven-sh/setup-bun@...), add a with block setting
no-cache: true to disable Bun executable caching. Ensure you apply these changes
to both occurrences of each action in the workflow so the runner and cache
hardening are consistent.

In `@CHANGELOG.md`:
- Around line 147-148: Update the inconsistent link target for the 0.1.0 entry
so it matches the 0.1.1 pattern: replace the npm URL used in the [0.1.0]
reference with the GitHub release tag URL (e.g., change
"https://www.npmjs.com/package/sitepass/v/0.1.0" to
"https://github.com/PeterM45/sitepass/releases/tag/v0.1.0") so both [0.1.1] and
[0.1.0] point to GitHub release tags.

In `@README.md`:
- Around line 206-217: The fenced code block under "CLI reference" that shows
the sitepass commands (the block containing "sitepass init  [--target
<name>]..." and "sitepass proxy --origin <url>...") needs a language identifier
to satisfy linters; update the opening triple backticks to include a language
such as text or sh (e.g., change ``` to ```text) so the CLI usage block is
annotated while keeping the content unchanged.

In `@src/cli.ts`:
- Around line 99-109: runInit() and the helpers readEnvValue()/upsertEnv() are
out of sync with loadDotenv(): they only match literal "KEY=" lines and don't
trim whitespace or strip surrounding quotes, causing duplicate keys and wrong
length checks; change readEnvValue and upsertEnv to reuse the same parsing logic
as loadDotenv() (or call a shared parser) so they accept optional whitespace
around '=' and strip matching single/double quotes from values, ensure
generateSecret()/secret assignment and the short-secret length check use the
parsed value (not the raw line), and apply the same fix to the other occurrences
noted (around the other blocks referenced).
- Around line 183-200: startProxy currently returns immediately after calling
server.listen, causing the CLI to log success before the socket is bound and
leaving no error handler; change start-up to capture and return the created
server, attach an 'error' handler to the server to handle bind failures, and
wait for the 'listening' event before printing the banner so you can read the
actual bound port from server.address() (handles ephemeral port 0 correctly) —
update the call site around startProxy/startProxy(...) to use the returned
server (or a Promise that resolves on 'listening') and emit the console.log only
after the server emits 'listening' using server.address() for the port, and
ensure errors are handled to avoid uncaught exceptions.

In `@src/core.ts`:
- Around line 199-205: The current call to options.onAuthFailure(request) passes
the full GateRequest (including urlencoded form body and cookies) which may leak
secrets; change the call to pass a redacted/filtered object or change the
callback type to accept non-secret metadata only (e.g., { method, url/path,
headersExceptCookies, ip, timestamp, userAgent, maybe usernameOnlyIfNeeded })
instead of the raw GateRequest; locate the failure path around
isCorrectPassword(...) and replace the request argument with a constructed
redactedRequest that explicitly omits the form body and session/cookie fields
before invoking options.onAuthFailure, or update the onAuthFailure signature and
callers accordingly so no raw credentials are forwarded.
- Around line 298-305: The current encoded-segment guard
(/%(2[ef]|5c|25)/i.test(path)) does not reject encoded semicolons, allowing
inputs like "/assets/..%3b/secret" to bypass the later literal-segment checks;
update the early reject logic to also detect "%3B" (encoded ';') so these cases
are rejected before any publicPaths prefix matching. Modify the regex/test that
checks the path (the /%(...)/i test on the path variable) to include the 3B hex
code for semicolon and keep this check executed before the publicPaths/prefix
logic and the segment loop that examines '.' '..' backslashes and ';'.

In `@src/node-body.ts`:
- Around line 13-37: Add handlers for 'aborted' and 'close' on the incoming
request stream (in the function that currently registers req.on('data'...),
req.on('end'...), req.on('error'...)) that reject the promise when the client
disconnects mid-upload (guard the rejection with !req.complete to avoid false
positives). When resolving or rejecting (in the size > limit branch, end, error,
aborted or close handlers) ensure you mark the read as done and remove all
listeners (data, end, error, aborted, close) to avoid leaks; keep the existing
BodyTooLargeError path and reuse the same done/limit logic and req.pause
behavior but also clean up listeners there. Apply the same changes to the second
reader block referenced around lines 42-63 so both reader implementations handle
aborted/close and perform listener cleanup.

In `@src/proxy.ts`:
- Around line 27-34: CLIENT_CONTROLLED currently strips only x-forwarded-* but
leaves the RFC7239 Forwarded header and rebuilds x-forwarded-host from an
unvalidated Host, allowing spoofing; update CLIENT_CONTROLLED to also include
the 'forwarded' header and stop reconstructing forwarded host metadata from the
raw Host header. Instead, only synthesize or set
x-forwarded-host/x-forwarded-proto from a trusted configuration value (e.g.,
publicHost or allowedHostnames) or an explicit allowlist check; if no trusted
public-host is configured, drop forwarded host metadata entirely. Ensure the
code paths that previously rebuilt or trusted Host (search for uses of
CLIENT_CONTROLLED, any Host-to-x-forwarded-host logic, and the block that
handles forwarded headers) are changed to strip 'Forwarded' and
validate/synthesize using the trusted config.

---

Outside diff comments:
In @.github/workflows/publish.yml:
- Around line 22-24: Update the workflow steps that call actions/checkout@v4 and
oven-sh/setup-bun@v2: for the actions/checkout step (identified by uses:
actions/checkout@v4) add with: persist-credentials: false to prevent persisting
runner git credentials, and for the oven-sh/setup-bun step (uses:
oven-sh/setup-bun@v2) add with: no-cache: true to disable reuse/caching of the
Bun executable across runs; leave other steps (e.g., actions/setup-node)
unchanged.

In `@src/express.ts`:
- Around line 47-55: In the BodyTooLargeError branch inside the isLoginPost
handling, ensure the connection is closed after sending the 413: set the
response Connection: close header (e.g., via res.set('Connection', 'close') or
res.setHeader) before calling res.status(413).type('text/plain').send('Payload
too large'), and teardown the socket once the response is flushed (attach a
finish listener to destroy the request/response socket, e.g., res.on('finish',
() => req.socket.destroy())). This change should be applied around the
readRawBody error handling where BodyTooLargeError is caught so unread bytes
can't occupy a keep-alive connection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8aa57a55-5c7e-4157-a53b-aec497d01193

📥 Commits

Reviewing files that changed from the base of the PR and between f30e1f9 and 2f8a9cd.

📒 Files selected for processing (33)
  • .github/ISSUE_TEMPLATE/bug_report.yml
  • .github/PULL_REQUEST_TEMPLATE.md
  • .github/dependabot.yml
  • .github/workflows/ci.yml
  • .github/workflows/publish.yml
  • CHANGELOG.md
  • CONTRIBUTING.md
  • README.md
  • SECURITY.md
  • package.json
  • scripts/smoke-dist.mjs
  • src/astro.ts
  • src/bun.ts
  • src/cli.ts
  • src/cloudflare.ts
  • src/core.ts
  • src/express.ts
  • src/hono.ts
  • src/netlify.ts
  • src/next.ts
  • src/node-body.ts
  • src/proxy.ts
  • src/sveltekit.ts
  • src/web.ts
  • test/adapters.test.ts
  • test/cli.test.ts
  • test/cloudflare.test.ts
  • test/core.test.ts
  • test/fixtures/credentials.ts
  • test/proxy.test.ts
  • tsconfig.json
  • tsup.config.ts
  • vitest.config.ts

Comment thread .github/ISSUE_TEMPLATE/bug_report.yml Outdated
Comment thread .github/workflows/ci.yml
Comment on lines 28 to 29
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Harden CI: disable persisted checkout credentials and Bun executable cache

  • Lines 28 & 60 (actions/checkout): add with: persist-credentials: false to avoid leaving auth/SSH credentials on the runner.
  • Lines 29 & 61 (oven-sh/setup-bun): add with: no-cache: true to disable Bun executable caching.
Suggested patch
-      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
-      - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
+      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+        with:
+          persist-credentials: false
+      - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
+        with:
+          no-cache: true
...
-      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
-      - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
+      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+        with:
+          persist-credentials: false
+      - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
+        with:
+          no-cache: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
no-cache: true
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 28-28: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 29-29: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default

(cache-poisoning)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 28 - 29, Update the GitHub Actions
steps that use actions/checkout and oven-sh/setup-bun: for the actions/checkout
steps (identified by uses: actions/checkout@...), add a with block setting
persist-credentials: false to avoid leaving auth/SSH credentials on the runner;
for the oven-sh/setup-bun steps (identified by uses: oven-sh/setup-bun@...), add
a with block setting no-cache: true to disable Bun executable caching. Ensure
you apply these changes to both occurrences of each action in the workflow so
the runner and cache hardening are consistent.

Comment thread CHANGELOG.md
Comment on lines 147 to +148
[0.1.1]: https://github.com/PeterM45/sitepass/releases/tag/v0.1.1
[0.1.0]: https://www.npmjs.com/package/sitepass/v/0.1.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.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Inconsistent version link targets.

The 0.1.1 link points to a GitHub release tag, while 0.1.0 points to the npm package page. For consistency and to follow common changelog conventions, both should point to GitHub release tags (e.g., https://github.com/PeterM45/sitepass/releases/tag/v0.1.0) or to GitHub compare URLs showing the diff.

📝 Suggested fix for consistency
-[0.1.0]: https://www.npmjs.com/package/sitepass/v/0.1.0
+[0.1.0]: https://github.com/PeterM45/sitepass/releases/tag/v0.1.0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[0.1.1]: https://github.com/PeterM45/sitepass/releases/tag/v0.1.1
[0.1.0]: https://www.npmjs.com/package/sitepass/v/0.1.0
[0.1.1]: https://github.com/PeterM45/sitepass/releases/tag/v0.1.1
[0.1.0]: https://github.com/PeterM45/sitepass/releases/tag/v0.1.0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 147 - 148, Update the inconsistent link target for
the 0.1.0 entry so it matches the 0.1.1 pattern: replace the npm URL used in the
[0.1.0] reference with the GitHub release tag URL (e.g., change
"https://www.npmjs.com/package/sitepass/v/0.1.0" to
"https://github.com/PeterM45/sitepass/releases/tag/v0.1.0") so both [0.1.1] and
[0.1.0] point to GitHub release tags.

Comment thread README.md
Comment thread src/cli.ts
Comment thread src/core.ts
Comment thread src/core.ts Outdated
Comment thread src/node-body.ts
Comment thread src/proxy.ts
Comment on lines +27 to +34
// Headers the client must not control: the gate's own bypass credential, and the
// forwarded-request metadata, which a front-most proxy sets authoritatively.
const CLIENT_CONTROLLED = new Set([
'x-sitepass-bypass',
'x-forwarded-for',
'x-forwarded-proto',
'x-forwarded-host',
])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

The forwarded host/proxy metadata is still client-spoofable.

This only strips the x-forwarded-* family. A client can still send RFC 7239 Forwarded, and x-forwarded-host is rebuilt from the unvalidated Host header, so downstream code that trusts forwarded host/scheme/IP can still be poisoned. Either drop forwarded host metadata unless you have a trusted public-host config, or synthesize it from an allowlisted hostname and strip Forwarded alongside the legacy headers.

Also applies to: 153-171

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/proxy.ts` around lines 27 - 34, CLIENT_CONTROLLED currently strips only
x-forwarded-* but leaves the RFC7239 Forwarded header and rebuilds
x-forwarded-host from an unvalidated Host, allowing spoofing; update
CLIENT_CONTROLLED to also include the 'forwarded' header and stop reconstructing
forwarded host metadata from the raw Host header. Instead, only synthesize or
set x-forwarded-host/x-forwarded-proto from a trusted configuration value (e.g.,
publicHost or allowedHostnames) or an explicit allowlist check; if no trusted
public-host is configured, drop forwarded host metadata entirely. Ensure the
code paths that previously rebuilt or trusted Host (search for uses of
CLIENT_CONTROLLED, any Host-to-x-forwarded-host logic, and the block that
handles forwarded headers) are changed to strip 'Forwarded' and
validate/synthesize using the trusted config.

Comment thread test/proxy.test.ts
Security/correctness:
- core: onAuthFailure now receives a redacted { method, path } view instead of
  the full GateRequest — the hook is the natural place for logging, and the
  request carried the urlencoded login body (cleartext password) and the
  session cookie. New exported AuthFailure type.
- core: isPublicPath also rejects encoded semicolon (%3b) — an origin decoding
  it to ';' could reach the same '..;' path-parameter traversal the literal
  guard blocks.
- node-body: readers reject on 'aborted'/'close' arriving without 'error' (a
  client dropping mid-upload), which previously left the promise pending and
  hung the Express/proxy request. Both readers mark done on 'end' so a trailing
  'close' is a clean no-op. New test/node-body.test.ts.
- proxy: drops the RFC 7239 Forwarded header too (was only stripping the
  X-Forwarded-* family), so a client can't poison a downstream that trusts it.
- cli: proxy announces success only on 'listening' and surfaces a bind error
  (e.g. EADDRINUSE) instead of printing a false banner then crashing.
- cli: readEnvValue/upsertEnv/loadDotenv share one tolerant parser, so init
  recognizes a hand-edited 'KEY = "value"' line instead of regenerating the
  secret (and upsert updates it in place rather than appending a duplicate).

Polish:
- ci: persist-credentials: false on every checkout (don't leave repo creds on
  the runner).
- docs: issue-template SECURITY link is now absolute; README CLI block tagged
  'text'; README onAuthFailure example uses the redacted payload.
- test/proxy: the hop-by-hop/Forwarded stripping assertions now actually send
  proxy-authorization and Forwarded, so they can't pass vacuously.

Declined: CodeRabbit wanted CHANGELOG's 0.1.0 to link a release tag, but no
v0.1.0 git tag exists, so the npm link is the honest target.

105 tests; coverage 95.4/85.5/98.8/97.7; publint clean; attw 0 problems.
@PeterM45

PeterM45 commented Jun 9, 2026

Copy link
Copy Markdown
Owner Author

Thanks for the review — addressed in df563b5. Summary of dispositions:

Fixed (security / correctness):

  • onAuthFailure now receives a redacted { method, path } view, not the full GateRequest (which carried the cleartext login body and session cookie).
  • isPublicPath also rejects encoded semicolon %3b (decodes to the ..; path-param traversal the literal guard blocks).
  • node-body.ts readers reject on aborted/close without error, so a client dropping mid-upload no longer hangs the request (new test/node-body.test.ts).
  • Proxy bind: success banner only on listening, and EADDRINUSE/errors surfaced instead of a false banner + crash.
  • .env parsing unified across readEnvValue/upsertEnv/loadDotenv so init recognizes a hand-edited KEY = "value" line instead of regenerating the secret.
  • Proxy now strips the RFC 7239 Forwarded header too (was only X-Forwarded-*).

Fixed (polish):

  • persist-credentials: false on every checkout; issue-template SECURITY link made absolute; README CLI block tagged text; proxy test now actually sends proxy-authorization/Forwarded so the stripping assertions can't pass vacuously.

Declined:

  • CHANGELOG 0.1.0 link — there is no v0.1.0 git tag, so the npm package page is the honest target (a release-tag link would 404).
  • setup-bun no-cache: true — disabling the executable cache trades real CI time for a weak threat given --frozen-lockfile; kept the cache.

All green locally: 105 tests, coverage 95.4% stmts / 85.5% branch, publint clean, attw 0 problems.

@PeterM45 PeterM45 merged commit f3d53fc into main Jun 9, 2026
6 checks passed
@PeterM45 PeterM45 deleted the fix/audit-0.1.2 branch June 9, 2026 20:50
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