Skip to content

v0.6.1 — SSRF guard + audit fixes

Choose a tag to compare

@siaginw siaginw released this 04 Sep 23:00
· 2 commits to main since this release

SheetDiff v0.6.1 — the audit pass

The post-release audit of v0.6.0 found one HIGH security issue and one false claim in the changelog. Both corrected, plus the rest of the findings.

The HIGH: push URLs were a full SSRF surface

users.notify_url is a URL the server fetches — and it was fetched with no restrictions. The auditor proved, with live listeners, that a user could make the server POST to 127.0.0.1, ::1, cloud metadata (169.254.169.254), RFC1918 and ULA addresses, and non-canonical spellings like http://2130706433/; redirects were followed (a "public" URL could 302 into the LAN); and nothing checked DNS resolution, so rebinding was trivial.

Now: the hostname is resolved at send time and every resolved address is refused when it's loopback, link-local, RFC1918, ULA, unspecified, or IPv4-mapped IPv6; redirects are refused outright (redirect: "error"); non-canonical IPv4 spellings die at resolution. Deployers who deliberately run ntfy on the same LAN set NOTIFY_ALLOW_PRIVATE_URLS=1. Six unit tests pin the guard.

The false claim, corrected

The v0.6.0 changelog said parseCompletedDate validated via Temporal.PlainDate. It didn't — an aborted edit script meant the old day-count check shipped. Now it really is ported, with overflow: "reject" — critical, because Temporal's default is "constrain", which would clamp Feb 30 to Feb 28 and silently reintroduce the exact rollover bug this exists to kill. The full existing date battery plus the auditor's differential corpus pin the behavior.

The rest

  • Captures right after a GIS import stay quiet — their "changes" are the rows the office just imported; announcing them as new work was backwards.
  • The push is fire-and-forget: a dead ntfy endpoint can no longer add its 5-second timeout to a capture or to the sequential scheduler tick.
  • Pino redaction now covers nested credential paths (err.response.data.access_token, err.config.headers.authorization), and the token-persistence path logs err.message instead of raw gaxios error objects.
  • The billing page now surfaces an untracked TOTALS tab's over-placement exactly like the CSV and PDF always did.
  • The test-push button reports failure honestly; an invalid URL is rejected with a message instead of silently cleared; pure viewers no longer see the onboarding checklist (steps they can never complete).
  • knip-cleanups: pino-pretty removed, temporal-spec declared as the types dependency it is, dead exports dropped; pino externalized from the server bundle.

390 tests + 7 E2E, all green in CI. Node 22+.