Skip to content

Releases: logtide-dev/logtide

v1.0.3

Choose a tag to compare

@Polliog Polliog released this 26 Jun 15:20
8423202

A security-focused release. It resolves a batch of privately reported issues (coordinated disclosure via KIberblick.de): cross-tenant read on the dashboard API endpoints, stored XSS via OTLP service.name in the service map, an open redirect on the auth-free login/register path, a DNS-rebinding gap in the SSRF guard's HTTP path, a first-admin bootstrap promotion race, and a capability-limit check-then-act race; trace span attributes are now PII-masked as well, and service.name is sanitized at ingestion as defense in depth. No database migrations; drop-in upgrade. Alongside the security work: two correctness follow-ups from the multi-engine bug-hunt sweep (issue #255): Sigma detection now honors full SigmaHQ field-modifier chains, and the service-map p95 is a true window percentile on every storage engine. The storage-layer change was validated against real ClickHouse, MongoDB and TimescaleDB. This line also fixes two operational bugs: a Redis memory leak where completed/failed BullMQ jobs were never evicted, and a nightly SigmaHQ sync that re-imported the whole catalog as enabled and auto-created alert rules. Plus a few frontend touch-ups: theme-aware trace/session IDs in the log detail, per-occurrence trace links on the error page, metadata copy buttons, a breadcrumbs timeline and nested metadata columns in log search.

Security

  • Cross-tenant read on the dashboard API endpoints (fixed): the five dashboard endpoints (/api/v1/dashboard/stats, /timeseries, /top-services, /timeline-events, /recent-errors) plus the newer /activity-overview took organizationId from the query string and only ran the organization-membership check behind if (request.user?.id), which is set for session auth only. For API-key auth requireFullAccess lets any non-write key through without setting request.user, so the membership check was skipped and the org was read from the attacker-supplied query rather than the key's bound request.organizationId; when projectId was omitted the project-in-org check was skipped too. A holder of any full-access API key (bound to org A) could read another organization's dashboard data by passing that org's id. All six handlers now route through a shared resolveDashboardScope that, for API-key auth, requires the requested org to match the key's bound org and the requested project to match (defaulting to the key's bound project when omitted), mirroring resolveQueryProjectId which already protects the query and traces routes. Session auth keeps the org-membership and project-in-org checks. Reported privately via KIberblick.de
  • Stored XSS via OTLP service.name in the service map (fixed): service.name from ingested traces passed through sanitizeForPostgres, which only strips null bytes, so < > " ' survived into span.service_name and were served verbatim by the service-map API. In ServiceMap.svelte the ECharts tooltip.formatter returned a raw HTML string built from params.name / params.data.source / params.data.target, and ECharts renders tooltip output as HTML, so a service.name like <img src=x onerror=...> executed in the browser of any operator who opened the project's service map and hovered the node or edge. User-derived tooltip values are now HTML-escaped via a shared escapeHtml util (also adopted by the SIEM HTML report builder, replacing its private copy). Stored data is left raw on purpose (escaping at the sink, not the store, avoids double-encoding and keeps the JSON API correct). Reported privately via KIberblick.de
  • Open redirect on auth-free login/register (fixed): in authMode === 'none' deployments the login and register pages forwarded the user-supplied redirect query parameter via goto(redirectUrl) with no validation, while the normal submit path already checked it. The check now lives in a shared isSafeInternalPath/safeRedirect helper used by both the auth-free and normal paths on both pages; it requires a single-leading-slash path and rejects protocol-relative forms including the backslash variant (/\evil.com) that some browsers normalize to //. Reported privately via KIberblick.de
  • SSRF guard now pins the validated IP (DNS rebinding hardening): safeFetch resolved and validated the target host, then let fetch() re-resolve at connect time, leaving a resolve-then-connect window where a hostname could rebind to an internal address between check and connect (the TCP monitor path already pinned, the HTTP path did not). The HTTP(S) path now connects through a per-request undici dispatcher whose lookup is pinned to the already-validated address, so the socket reaches the exact IP that passed validation; TLS SNI and certificate validation still use the original hostname. Reported privately via KIberblick.de
  • First-admin bootstrap race (fixed): createUser decided the automatic first-admin promotion with a non-atomic hasAnyAdmin() check followed by a separate insert, so concurrent registrations in the zero-admin window could each observe "no admin yet" and all be promoted. The check-then-insert now runs inside a transaction holding a Postgres advisory lock, so at most one registration wins the promotion. Only reachable before the first admin exists (and closed entirely when INITIAL_ADMIN_* is set). Reported privately via KIberblick.de
  • Capability limit check-then-act race (fixed): resource-creating routes (api keys, custom dashboards, alert rules, sigma rule import/enable, notification channels) ran COUNT -> assertWithinLimit -> insert without serialization, so parallel requests could each read a count under the limit and then all insert, exceeding a configured finite cap (a quota bypass, not a tenant boundary; the OSS default has no finite limits). The count+create now runs through a shared withLimitLock helper that takes a per-organization, per-capability transaction-scoped advisory lock, so concurrent creators of the same resource type serialize and the cap holds. Reported privately via KIberblick.de
  • Defense-in-depth on OTLP service.name at ingestion: complementing the service-map output-encoding fix above, ingested service.name (for logs, spans and metrics) is now run through a shared sanitizeServiceName that strips control characters (C0/DEL/C1, including null bytes) and caps the length, while deliberately preserving otherwise legitimate characters (escaping still happens at each sink). This keeps a raw payload from resurfacing through a sink that is added later or forgets to encode. Suggested by KIberblick.de
  • PII masking now also covers trace span attributes: masking was wired only into log ingestion, so trace spans were stored with their attributes verbatim. Spans routinely carry http.request_body / http.response_body (plaintext credentials, JWTs), net.peer.ip and user agents, all persisted unmasked. tracesService.ingestSpans now runs the same org/project masking rules over each span's attributes, resourceAttributes and event/link attributes before storage, and drops (fail-closed) any span whose masking throws. Because request/response bodies are opaque stringified JSON that field-name rules can't see into, those body attributes are deep-masked (parse the JSON, mask password/token/email inside, re-serialize) with full redaction as a fallback when the value is not parseable JSON. Metric attributes are not yet masked (tracked separately)

Added

  • Per-occurrence trace links on the error detail page: each log in an error group's Logs tab now shows a "View Trace" action when that log carries a trace context, opening the existing trace timeline. The error-group logs endpoint (GET /api/v1/error-groups/:id/logs) now surfaces the traceId it already loaded from storage and previously discarded; no schema change, no migration
  • Copy buttons on metadata blocks: the log search expanded detail and the Log Context dialog now have a one-click copy on each metadata block (with copied feedback), so a log's metadata JSON can be grabbed without selecting it by hand
  • Breadcrumbs timeline in the log search detail: when a log carries metadata.breadcrumbs, the expanded row now renders a collapsible "Breadcrumbs (N)" timeline (the same BreadcrumbTimeline view already used in the Log Context dialog) instead of leaving them buried in the raw metadata JSON
  • Nested metadata columns: custom metadata columns in log search now accept dot-notation paths (e.g. sdk.name) to read into nested objects. Exact top-level keys still win first, so flat keys that contain dots (e.g. debug.trace_id) keep resolving; object/array values render as compact JSON, and the full value is available on hover

Fixed

  • Admin usage page returned 403 for organizations the admin wasn't a member of: the metering endpoints (/usage, /usage/breakdown, /usage/storage, /usage/capabilities) gated solely on org membership, so the platform Admin > Usage page (which lists every organization) got "Forbidden" whenever a selected org wasn't one the admin personally belonged to. Platform admins (is_admin) now bypass the membership check on these read endpoints; the queries stay filtered by the requested organizationId, so tenant scoping is unchanged
  • Trace volume / trace latency dashboard panels were empty on ClickHouse and MongoDB: both panel fetchers read span data straight from the Postgres spans hypertable and its continuous aggregates and short-circuited to an empty series when reservoir.getEngineType() !== 'timescale', so on ClickHouse/MongoDB deployments (where spans live in those engines) the panels returned no data. Added a multi-engine reservoir.getSpanTimeseries (time-bucketed span volume + true window p50/p95/p99 from raw spans: percentile_cont on TimescaleDB, quantile on ClickHouse, $percentile on MongoDB) and switched both fetchers to it. The ClickHouse and MongoDB paths mirror the validated getServiceHealthStats percentile approa...
Read more

v1.0.2

Choose a tag to compare

@Polliog Polliog released this 21 Jun 13:52
45fee97

A frontend correctness and security release from a comprehensive multi-agent frontend bug hunt (UI, logic, reactivity, leaks and security), plus a hardening of how the browser authenticates the live-streaming endpoints. The headline item is single-use stream tickets: the session token no longer travels in WebSocket/SSE URLs (where reverse proxies log it). One additive database migration (049_stream_tickets); otherwise a drop-in upgrade.

Security

  • Session token no longer placed in WebSocket/SSE URLs: browser WebSocket and EventSource cannot send an Authorization header, so the log live-tail (/api/v1/logs/ws), the SIEM events stream (/api/v1/siem/events) and the trace live-tail (/api/v1/traces/stream) previously carried the long-lived session token in the URL query string, where it is logged by proxies and servers. The client now mints a short-lived, single-use stream ticket via an authenticated POST /api/v1/stream-tickets and passes that ticket instead. Tickets live in the relational database (not Redis, so the mechanism is portable across the BullMQ and graphile queue backends), expire in 30s and are consumed on first use. The legacy ?token= path still works for backward compatibility
  • Webhook channel secrets no longer rehydrated into the DOM: editing a notification channel no longer pre-fills the bearer token / basic-auth password inputs with the stored secret; the fields stay empty with a "leave blank to keep current" hint and are only sent when the user types a new value
  • OIDC callback strips the session token from the URL after reading it, so it no longer lingers in browser history, the referrer or logs
  • Admin pages enforce a client-side admin guard: several admin views (user detail, usage, organization detail) loaded and could mutate data on mount without checking the admin role; they now redirect non-admins, and the admin section layout has a guard of its own
  • Removed a debug console.log that leaked log message content and api-key metadata to the browser console on the error-detail page

Added

  • Global 401 handler: a single fetch interceptor installed at app startup clears local auth state and redirects to the login page (preserving the current path so the user lands back there after signing in) on any authenticated /api/v1 401 that is not an auth endpoint. Previously a revoked or expired session was only detected on a full dashboard remount, so a logged-out user could keep clicking around getting silent failures
  • POST /api/v1/stream-tickets endpoint and stream_tickets table (migration 049) backing the stream-ticket auth described above

Fixed

  • Stale-response races: overlapping loads triggered by fast filter/pagination changes could let an older in-flight response overwrite fresher results. Added local request-sequence guards on the log search, traces list, error groups, SIEM incidents, monitor detail/list, custom-dashboard panels and alert-preview views
  • API client error handling: error-branch response.json() calls are guarded so a non-JSON error body (reverse-proxy 502/HTML, empty 204) no longer throws a SyntaxError that masks the real HTTP failure (auth, admin and exceptions clients)
  • Locale-stable formatting: user-facing dates and numbers now use explicit en-US formatting across the status pages, members, project settings, traces, metrics, search and notification-channel views; alert-history timestamps no longer label UTC values as if they were local time
  • Lifecycle and memory leaks: component store subscriptions are auto-managed, a first-run shortcut-hint setTimeout is cleared on unmount, and chart instances are disposed, so navigating away no longer leaves timers, listeners or subscriptions behind
  • Svelte 5 reactivity and assorted UI fixes: the trace detail page reloads when navigating between traces; the api-key DSN preserves an http:// scheme for non-TLS deployments; the "View Error Group" action navigates with a param the target page actually reads; SigmaSync no longer crashes when a commit hash is absent; the toaster follows the app theme; the delete-organization confirm is disabled while in flight; PII masking rules require a regex or field names; numeric monitor inputs guard against NaN; and the audit-log resource cell no longer renders a literal escape sequence
  • ClickHouse: materialized-view backfills now run once instead of on every startup

Notes

  • Left intentionally unchanged: storing the session token in localStorage (a disputed, low-severity finding). Moving it to an httpOnly cookie would trade XSS token-theft for CSRF surface and a full auth-model overhaul without a clear net win; the high-leverage XSS defenses (CSP, output sanitization, auditing the few {@html} sites) are tracked separately

v1.0.1

Choose a tag to compare

@Polliog Polliog released this 19 Jun 20:15

A security and correctness release from a comprehensive, multi-engine bug audit of the 1.0 line. The headline items are two cross-tenant data-exposure fixes that were live in 1.0.0, alongside a broad sweep of detection, ingestion, storage, alerting and frontend correctness fixes. No database migrations; this is a drop-in upgrade. The storage-layer fixes were validated against real ClickHouse and MongoDB (and TimescaleDB), and CI now runs the MongoDB reservoir integration suite.

Security

  • Cross-tenant log exposure via the WebSocket live-tail (GET /api/v1/logs/ws): the handler validated only the session token and then streamed whatever projectId the client passed, never checking project/organization membership, so any authenticated user could live-tail any project's logs by supplying its id. It now enforces the same verifyProjectAccess membership check as the REST query routes (regression test added). Was live on the 1.0.0 line
  • Cross-tenant leak of notification-channel secrets: GET /notification-channels/alert-rules/:id and /sigma-rules/:id returned the channel config verbatim (including webhook auth tokens/passwords) with no membership check. They now resolve the rule's organization, require membership, and scope the read to that org
  • PII masking fail-open: maskText skipped all content rules for pure-alphanumeric strings, so a separator-less credit-card number (e.g. 4111111111111111) was stored unmasked. The all-alphanumeric early-exit is removed; only too-short strings are skipped
  • OIDC account-takeover: linking an external identity to an existing local account by email now requires the provider to assert the email is verified (email_verified === true), preventing takeover via an unverified IdP email; LDAP is treated as authoritative
  • Cross-tenant ?projectId / association gaps closed: the correlation batch-identifiers endpoint now intersects the requested project with the caller's accessible projects (and requires read access); status-incident and scheduled-maintenance creation, organization-default channels, and alert/sigma/monitor channel associations now validate that the project/channels belong to the caller's organization
  • Session invalidation: disabling a user, resetting their password, and changing a user's admin role now invalidate the cached session (previously the cached profile stayed valid for up to the cache TTL); the frontend logout now revokes the server-side session token instead of only clearing local state
  • Hardening: emails are stored and compared case-insensitively across registration/login/profile-update; the global CACHE_TTL override no longer clamps semantic TTLs (sessions, OIDC state, settings); the webhook envelope id is validated as a UUID; SigmaHQ category sync matches on a directory boundary; OTLP trace/span ids of invalid length are rejected

Security

  • Dependency security updates, third wave (Dependabot): two further advisories resolved to their patched releases. The direct dependency nodemailer is bumped from ^8.0.9 to ^9.0.1 (GHSA-p6gq-j5cr-w38f, HIGH): the message-level raw option bypassed disableFileAccess/disableUrlAccess, enabling arbitrary file read and full-response SSRF in the delivered message; our SMTP senders only use standard createTransport/sendMail fields and never pass raw, but the dependency is patched regardless. The transitive undici (pulled in only by jsdom in the frontend test toolchain) is pinned via the root pnpm overrides to >=7.28.0 <8 (GHSA-vmh5-mc38-953g HIGH, TLS certificate validation bypass via dropped requestTls in the SOCKS5 ProxyAgent; GHSA-pr7r-676h-xcf6 MEDIUM, cross-user information disclosure via shared-cache whitespace bypass), resolving to 7.28.0 and staying on the 7.x line jsdom@29 expects. No vulnerable version remains in pnpm-lock.yaml

Fixed

  • ClickHouse "Query with id = ... is already running" under concurrent queries (#213 regression): the request-context propagation derived the ClickHouse query_id deterministically from requestId + operation, so two same-operation queries running concurrently within one request reused the same id and ClickHouse rejected the second. The query_id now keeps the request id + operation as a readable prefix (correlation is also carried in the SQL log_comment) and appends a random suffix so every query is unique. ClickHouse-only
  • Sigma condition operator precedence: parseExpression folded AND/OR strictly left-to-right, so a or b and c evaluated as (a or b) and c. AND now binds tighter than OR, matching the Sigma spec
  • Infinite loop on ingestion: a custom identifier pattern that can match the empty string (e.g. \d*) no longer hangs the worker (zero-width-match guard plus a forced global flag)
  • Trace summary races: upsertTrace is now race-free on TimescaleDB (single atomic INSERT ... ON CONFLICT) and ClickHouse (the summary is recomputed from the spans table instead of read-modify-write), so concurrent span batches no longer lose span counts
  • ClickHouse: added the missing session_id column (was written and filtered on but did not exist), parse zone-less datetimes as UTC, report the true latest value in the metrics overview (was the average) with NaN guards
  • MongoDB: deleteMetricsByTimeRange only deletes the matched metrics' exemplars (was all exemplars in the window); the metrics overview sorts before $last; metadata distinct no longer drops numeric/boolean values; ingestReturning returns the rows that did land on a partial bulk-write failure
  • Rate-of-change alerts: the baseline now applies the rule's metadata filters (previously only the current rate did, deflating the deviation ratio), and a minBaselineValue of 0 is honored instead of defaulting to 10
  • Dashboards: personal dashboards are scoped to their creator (no longer readable/editable by any org member); metric_stat sums across buckets for sum/count aggregations; top-error percentages are computed against the true total; the capability usage page renders a zero limit as fully restricted, not unlimited
  • Pagination/counts: admin organization/project search counts apply the search filter; correlation lookups report the true total; the ClickHouse trace error-rate uses a consistent denominator; getTopServices honors the to bound
  • Live tail: SSE polling bypasses the 60s query cache and guards against overlapping polls; the live-tail WebSocket URL works behind a reverse proxy; the traces and search live tails no longer corrupt pagination or index-keyed row state
  • Infra/correctness: BullMQ and graphile queue retry-attempt defaults are aligned; webhook deliveries can only be replayed from the terminal dead state (no double delivery); the audit flush buffer is bounded on persistent DB failure; alert-notification jobs are deduplicated by history id; admin org-role changes are restricted consistently with member removal; tenant-table updates in the log-pipeline and service-dependency queries are project-scoped

Changed

  • The reservoir storage abstraction gains getTraceServices (distinct trace services with no result cap) implemented on all three engines and used by the traces service, replacing the previous 10k-trace paging
  • CI now provisions a MongoDB service for the reservoir integration suite, so all three storage engines are exercised on every run

Notes

  • Deferred to a follow-up (#255): Sigma compound field modifiers (field|base64|contains chains) and a true service-map p95 (needs a mergeable t-digest sketch in the continuous aggregate)
    Full Changelog: v1.0.0...v1.0.1

v1.0.0

Choose a tag to compare

@Polliog Polliog released this 16 Jun 06:45
51aec1c

First stable release of the 1.0 line. The headline work since 0.9.7 is the tenant data isolation audit that hardens every backend data-access path, and the metering + capability system that gives every organization usage measurement, feature gates and enforceable limits/quotas (ingestion, spans, storage) without changing OSS behavior. A typed lifecycle hooks surface and a reusable outbound webhook delivery system (HMAC signing, retry/backoff, DLQ, centralized SSRF) land alongside.

See CHANGELOG.md for the full, detailed record.

Security

  • Tenant data isolation audit (#219, #228): closed authenticated cross-tenant log and trace/span reads via unvalidated ?projectId, swept application-layer scoping gaps on tenant-table queries, and added an isolation test suite plus CI tripwires (check:tenant-scoping). Reservoir log query params now require projectId with an explicit GLOBAL_SCOPE sentinel.
  • PII masking is fail-closed at ingestion: records whose masking fails are rejected before storage and reported in the ingest response rejected[]; no unmasked data can reach any storage engine.
  • Two waves of dependency advisory fixes (12 advisories): vitest, esbuild, shell-quote, nodemailer, vite, js-yaml, protobufjs, form-data and @opentelemetry/core upgraded to patched releases; no vulnerable version remains in the lockfile.

Added

  • Capability system (#214): per-organization feature gates, static limits and metered usage quotas, enforced across alerts, sigma rules, notification channels, API keys, dashboards and ingestion/storage/span quotas. OSS defaults stay unlimited.
  • Resource usage metering (#212): storage-agnostic per-org/project consumption tracking with a Usage dashboard, plus span and storage-snapshot recording sites and capability-usage vs plan-limit progress bars.
  • Lifecycle hooks (#216): typed before-/after- extension points for ingestion, query, alert evaluation and webhook dispatch; no-op in OSS, configurable via HOOKS_MODULES.
  • Generic outbound webhook delivery (#218): HMAC-SHA256 signing, exponential-backoff retry, a dead-letter queue, per-org concurrency limiting and centralized SSRF protection, with every sender migrated onto it and a unified event envelope.
  • Audit log primitive (#217): typed actions/actors/outcomes, per-org retention, and audit coverage for API-key access and failed logins.
  • Request context propagation (#213): AsyncLocalStorage-backed context across HTTP, jobs and the DB layer.

Changed

  • BREAKING: unified webhook event envelope (#218): every outbound delivery serializes to one { id, type, version, occurredAt, organizationId, projectId, data } envelope with an X-Logtide-Event-Version: 1 header.
  • OTLP log metadata shape: resource attributes now land under metadata.resource; structured bodies preserved under metadata['otel.body'].

Fixed

  • Sigma search by MITRE technique/tactic/tag (text[] vs jsonb), a migration prefix collision that could break production migrate, and assorted silent-failure and status-code issues across the admin, monitoring and sigma surfaces.

Notes

  • Scheduled email digest reports (#154) are merged as groundwork but disabled in this release pending completion.

Full Changelog: v0.9.7...v1.0.0

v1.0.0-rc1

v1.0.0-rc1 Pre-release
Pre-release

Choose a tag to compare

@Polliog Polliog released this 12 Jun 10:22
636dca0

What's Changed

  • Generic outbound webhook delivery infrastructure (#218) by @Polliog in #242
  • ingestion safety: fail-closed pii masking + health visibility by @Polliog in #243
  • capability enforcement: sigma, channels, api keys, dashboards by @Polliog in #244
  • unified webhook event envelope + after lifecycle hooks by @Polliog in #245
  • traces/otlp gap closure: metadata shape, frontend tests, e2e by @Polliog in #246
  • test debt: drop all coverage exclusions, shared schemas, ci wiring by @Polliog in #247
  • audit primitive: typed actions, actor types, outcomes, per-org retention by @Polliog in #248

Full Changelog: v1.0.0b-beta2...v1.0.0-rc1

v1.0.0b-beta2

v1.0.0b-beta2 Pre-release
Pre-release

Choose a tag to compare

@Polliog Polliog released this 09 Jun 13:30

What's Changed

  • Capability system: feature gates, limits and usage quotas (#214) by @Polliog in #238
  • Span and storage metering recording sites (#212 follow-up) by @Polliog in #239
  • lifecycle hooks at ingestion, query, alert evaluation and webhook dispatch (#216) by @Polliog in #241

Full Changelog: v1.0.0b-beta1...v1.0.0b-beta2

v1.0.0b-beta1

v1.0.0b-beta1 Pre-release
Pre-release

Choose a tag to compare

@Polliog Polliog released this 05 Jun 22:56

What's Changed

New Contributors

Full Changelog: v0.9.7...v1.0.0b-beta1

v0.9.7

Choose a tag to compare

@Polliog Polliog released this 03 Jun 21:40

Security

  • SSRF in alert/Sigma webhook delivery via the legacy delivery path (authenticated) (GHSA-7v53-pw6r-99vj, CWE-918): the 0.9.6 SSRF hardening added the centralized utils/ssrf-guard.ts guard and wired it into the HTTP/TCP monitors and the WebhookProvider, but the actual alert/Sigma webhook delivery path was left on the old inline filter. sendWebhookNotification in queue/jobs/alert-notification.ts (reached by the alert-notifications BullMQ worker for threshold, rate-of-change and Sigma-rule alerts) still ran a bare fetch(webhook_url, …) guarded only by a string-based isPrivateIP(). That check was bypassable: any non-dotted-quad hostname returned false (no DNS resolution), so a domain whose A record points at 127.0.0.1 / 169.254.169.254 / an internal host passed the filter and the resolved internal address was then connected; the bare fetch used the default redirect: 'follow', so a public host that 302s to an internal URL was followed straight there; and CGNAT (100.64.0.0/10), IPv6, IPv4-mapped IPv6, 0.0.0.0/8 and 198.18.0.0/15 were not covered. An authenticated org owner/admin who can create a webhook notification channel could therefore use the backend as a blind SSRF probe against internal services and cloud metadata, with a partial read-back oracle since a non-2xx internal response body was spliced into the alert-history error message. The guarded "Test" button already blocked the same URLs, confirming this as an incomplete-fix sibling-gap. Fix: sendWebhookNotification now routes delivery through safeFetch(url, init, { allowPrivate: config.MONITOR_ALLOW_PRIVATE_TARGETS }), exactly as WebhookProvider does (DNS resolution + per-redirect-hop revalidation + full IPv4/IPv6 private/reserved range coverage), mapping SsrfBlockedError to the existing "private/internal addresses are not allowed" error. The inline isPrivateIP/BLOCKED_HOSTS filter and the bare fetch are removed, and blocked targets are now rejected before the response body is read (closing the read-back oracle). The MONITOR_ALLOW_PRIVATE_TARGETS opt-in still lets self-hosted deployments target internal endpoints. Reported by tonghuaroot

Full Changelog: v0.9.6...v0.9.7

v0.9.6

Choose a tag to compare

@Polliog Polliog released this 01 Jun 15:56

Changed

  • Frontend now runs as a full SPA on adapter-node instead of doing SSR with client hydration. A single export const ssr = false in the new root +layout.ts cascades to every route, so the server only ships the empty app shell and the client renders from scratch. Eliminates the entire class of hydration mismatch bugs that had been accumulating across login, register, invite, public status page, the /auth/callback page and the various dashboard subpages, each previously patched with its own per-route ssr = false. Server load functions and the Node runtime are untouched (the adapter, API proxying, env vars, BullMQ etc. keep working exactly as before); there are no +page.server.ts / +layout.server.ts files in the repo today so nothing had to change semantically on the server side. UX tradeoff: the public status page at /status/[orgSlug]/[projectSlug] now flashes the empty shell for one paint before the JS hydrates, which is acceptable for an authenticated-by-default product but should be revisited if SEO on the status page becomes a goal (a single-line override export const ssr = true in that page's +page.ts puts it back on SSR without affecting anything else)
  • Removed 22 redundant route-level ssr = false declarations (dashboard/+layout.ts, dashboard/admin/+layout.ts and 20 +page.ts files across landing, login, register, onboarding, invite, status, auth callback and the dashboard search / metrics / alerts / monitoring / projects / sessions / settings subtrees) that had been added one by one as each page hit a hydration bug. The new root-level cascade makes them all dead config; deleting them removes the temptation to copy the pattern into new routes

Fixed

  • Infinite skeleton spinner on /dashboard/search, /dashboard/traces and /dashboard/metrics when no project in the org had its data-availability flag set, typically right after a user deleted the only projects that had been ingesting. The filter logic at search/+page.svelte:441-444 (and the identical pattern at traces/+page.svelte:142-145 and metrics/+page.svelte:90-93) read const logsProjectIds = availability?.logs and then branched on logsProjectIds ? filter : fallback. When getProjectDataAvailability legitimately returned { logs: [] } the empty array took the truthy branch ([] is truthy in JS) and [].includes(p.id) excluded every project, so the displayed project list was empty, loadLogs() never fired, and hasLoadedOnce stayed false so the SkeletonTable rendered forever. Fix: guard the truthy branch with logsProjectIds && logsProjectIds.length > 0 so an empty availability response falls back to "show all projects" exactly like the API-failure path (.catch(() => null)) already does. The 0.9.4 backend optimization that introduced the cached availability flags is unaffected; this is purely a frontend null-vs-empty conflation. Logs of already-hard-deleted projects on the TimescaleDB engine remain unrecoverable due to ON DELETE CASCADE on logs.project_id (tracked separately under the soft-delete projects epic)
  • OIDC login failed against issuer-identifying providers (e.g. Authelia) because the iss callback parameter was dropped (#233, #234): the OIDC callback handler extracted only code and state from the provider redirect and rebuilt the callback URL from just those two, discarding everything else. Providers implementing RFC 9207 (OAuth 2.0 Authorization Server Issuer Identification) append iss to the redirect and openid-client's authorizationCodeGrant() validates it, so the token exchange failed with "issuer parameter missing" and login never completed. The Fastify callback route now forwards the full request.query through handleOidcCallback into the provider, which replays every parameter onto the callback URL handed to the token exchange (so iss, session_state, etc. survive); the required code/state are always re-asserted from the validated values. Duplicated/array-valued query params are collapsed to a single value with searchParams.set instead of being appended, since OIDC authorization-response params are single-valued per RFC 6749 / RFC 9207, avoiding a malformed URL with duplicate iss/code reaching authorizationCodeGrant. Covered by new tests across the route, service and provider layers, including the array-collapse and undefined-param branches

Security

  • Cross-tenant project access via unvalidated projectId (authenticated): several routes accepted both an organizationId and a projectId, verified the caller was a member of the organization, but never verified that the supplied project actually belonged to that organization. Since project UUIDs are normal identifiers that appear in dashboard URLs and client API calls, a user could pair their own organizationId (membership check passes) with a victim's known projectId and reach another tenant's data. Confirmed on four handlers: POST /api/v1/alerts/preview returned sampleLogs (time, service, level, message, trace ID) from the foreign project; POST /api/v1/alerts and POST /api/v1/monitors let a rule/monitor be scoped to a foreign project (the monitor case also surfaces on the victim's public status page, which renders by project_id); and GET/DELETE /api/v1/sourcemaps let a member list or delete another tenant's source maps. Fix: a shared projectsService.projectBelongsToOrg(projectId, organizationId) helper (single projects lookup filtered on both id and organization_id) is now enforced right after the existing membership check on each of those routes, returning 403 when the project is foreign. The alert/monitor update paths don't accept a projectId and the custom-dashboards panel pipeline already had an equivalent ensureProjectInOrg guard at its choke point, so no change was needed there. Regression tests cover each handler
  • Server-side request forgery (SSRF) and internal port scanning via monitors and webhooks (authenticated): HTTP/TCP uptime monitors and webhook delivery executed user-supplied targets from the backend's network with no meaningful destination validation. Monitor creation only checked that an HTTP target started with http(s):// and that a TCP target contained :; checker.ts then called fetch(target, { redirect: 'follow' }) and createConnection({ host, port }), so a registered user could point a monitor at http://169.254.169.254/…, http://127.0.0.1, 10.0.0.0/8, etc. and use the sanitized up/down result and timing to probe internal services. The webhook provider had only literal-string private-IP filtering (no DNS resolution, incomplete IPv6, followed redirects), leaving DNS- and redirect-based bypasses open. Fix: a centralized outbound guard (utils/ssrf-guard.ts) resolves hostnames and rejects loopback, private, link-local (incl. 169.254.169.254 cloud metadata), CGNAT (100.64.0.0/10), multicast and reserved IPv4/IPv6 ranges (including IPv4-mapped IPv6 and ULA/fc00::/7, link-local fe80::/10). TCP checks resolve-then-pin the socket to the validated address (closing DNS-rebinding between validation and connect); HTTP checks and webhook delivery follow redirects manually and revalidate every hop instead of redirect: 'follow'. The guard runs both at monitor create/update time (immediate 400 feedback) and at execution time (authoritative, returns a blocked result). Private/internal targets are denied by default; self-hosted deployments that legitimately monitor internal services can opt back in with MONITOR_ALLOW_PRIVATE_TARGETS=true, which also governs webhook delivery. Note: HTTPS does not yet pin the connected address against a custom dispatcher, so a narrow DNS-rebinding window remains for HTTP monitors (tracked for a follow-up); the reported direct-target and redirect-to-internal vectors are closed

New Contributors

Full Changelog: v0.9.5...v0.9.6

v0.9.5

Choose a tag to compare

@Polliog Polliog released this 26 May 17:06
455f83b

Fixed

  • Metadata filters were silently ignored on ClickHouse and MongoDB (#226, issue #224): the metadata filter operators (equals, not_equals, in, not_in, exists, not_exists, contains) were only translated by the TimescaleDB query builder. The ClickHouse and MongoDB query translators never read params.metadataFilters, so any log search or alert rule that relied on a metadata.* filter came back unfiltered on those engines (the filter appeared to do nothing). ClickHouse now translates each filter to a predicate over the JSON metadata column using JSONExtractString paired with JSONHas to distinguish a missing key from an empty string (not_in/not_equals split on whether the key is present, contains uses positionCaseInsensitive). MongoDB builds one clause per filter keyed on metadata.<key>, all wrapped in $and so repeated filters on the same key don't overwrite each other, with include_missing controlling whether not_equals/not_in also match documents where the field is absent ($exists). Covered by new per-engine translator tests
  • Error notifications spammed one email per occurrence: processErrorNotification sent an in-app notification, email and webhook for every exception occurrence, suppressed only when the error group's status was ignored. A high-frequency client error (e.g. a Svelte effect_update_depth_exceeded loop firing thousands of times) produced one exception row per occurrence and therefore thousands of identical alert emails. The job now throttles per error group: it atomically claims a notification slot via UPDATE error_groups SET last_notified_at = now() WHERE status != 'ignored' AND (last_notified_at IS NULL OR last_notified_at <= cutoff) RETURNING id, so only the first occurrence inside the cooldown window notifies and the rest are skipped. The conditional UPDATE is race-safe (concurrent jobs serialize on the row lock and re-evaluate the predicate against the freshly written timestamp), so even a burst of thousands collapses to a single notification per window. Cooldown is configurable via ERROR_NOTIFICATION_COOLDOWN_MINUTES (default 15, set 0 to notify on every occurrence). Occurrence counts on the error group and in-app dashboards are unaffected
  • monitoring rejected by the notification-channels defaults endpoint: PUT/GET /api/v1/notification-channels/defaults/:eventType validated :eventType against a local Zod enum of ['alert', 'sigma', 'incident', 'error'] that was missing monitoring, even though the shared NotificationEventType type, the DB organization_default_channels constraint (migration 037) and the service all support it. Setting a default monitoring channel returned 400 Validation error ("received 'monitoring'"). Added monitoring to the route enum so the five event types are consistent across the stack

Added

  • Migration 043 error_notification_throttle: adds a nullable last_notified_at TIMESTAMPTZ column to error_groups, used as the per-group notification cooldown anchor (see the error-notification throttle fix above)

Security

  • Resolved 19 Dependabot advisories (8 high, 11 moderate) by bumping direct dependencies and pinning patched versions through the root pnpm overrides. protobufjs7.6.1 (kept on the 7.x line via >=7.5.8 <8; covers code injection, prototype-pollution gadget, unbounded-recursion DoS, unsafe option paths, crafted-field DoS, overlong UTF-8 in @protobufjs/utf8 >=1.1.1). kysely0.28.17 (bounded >=0.28.17 <0.29; JSON-path traversal injection in JSONPathBuilder.key()/.at()). svelte5.55.9 (>=5.55.7; SSR XSS via spread attributes and promise serialization, DOM-clobbering XSS, <svelte:element> ReDoS). @sveltejs/kit2.61.1 (>=2.60.1; query.batch cross-talk). fast-uri3.1.2 (path traversal + host confusion via percent-encoded segments). qs6.15.2 (DoS in qs.stringify), devalue5.8.1 (sparse-array DoS), brace-expansion5.0.6 (numeric-range DoS), ws8.21.0 (uninitialized memory disclosure). protobufjs and kysely were deliberately held on their current major/minor lines (their "latest" is a breaking jump) while still landing on the patched release