Releases: logtide-dev/logtide
Release list
v1.0.3
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-overviewtookorganizationIdfrom the query string and only ran the organization-membership check behindif (request.user?.id), which is set for session auth only. For API-key authrequireFullAccesslets any non-write key through without settingrequest.user, so the membership check was skipped and the org was read from the attacker-supplied query rather than the key's boundrequest.organizationId; whenprojectIdwas 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 sharedresolveDashboardScopethat, 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), mirroringresolveQueryProjectIdwhich 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.namein the service map (fixed):service.namefrom ingested traces passed throughsanitizeForPostgres, which only strips null bytes, so< > " 'survived intospan.service_nameand were served verbatim by the service-map API. InServiceMap.sveltethe EChartstooltip.formatterreturned a raw HTML string built fromparams.name/params.data.source/params.data.target, and ECharts renders tooltip output as HTML, so aservice.namelike<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 sharedescapeHtmlutil (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-suppliedredirectquery parameter viagoto(redirectUrl)with no validation, while the normal submit path already checked it. The check now lives in a sharedisSafeInternalPath/safeRedirecthelper 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):
safeFetchresolved and validated the target host, then letfetch()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):
createUserdecided the automatic first-admin promotion with a non-atomichasAnyAdmin()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 whenINITIAL_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 -> insertwithout 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 sharedwithLimitLockhelper 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.nameat ingestion: complementing the service-map output-encoding fix above, ingestedservice.name(for logs, spans and metrics) is now run through a sharedsanitizeServiceNamethat 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.ipand user agents, all persisted unmasked.tracesService.ingestSpansnow runs the same org/project masking rules over each span'sattributes,resourceAttributesand 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, maskpassword/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 thetraceIdit 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 sameBreadcrumbTimelineview 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 requestedorganizationId, 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
spanshypertable and its continuous aggregates and short-circuited to an empty series whenreservoir.getEngineType() !== 'timescale', so on ClickHouse/MongoDB deployments (where spans live in those engines) the panels returned no data. Added a multi-enginereservoir.getSpanTimeseries(time-bucketed span volume + true window p50/p95/p99 from raw spans:percentile_conton TimescaleDB,quantileon ClickHouse,$percentileon MongoDB) and switched both fetchers to it. The ClickHouse and MongoDB paths mirror the validatedgetServiceHealthStatspercentile approa...
v1.0.2
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
WebSocketandEventSourcecannot send anAuthorizationheader, 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 authenticatedPOST /api/v1/stream-ticketsand 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.logthat 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/v1401 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-ticketsendpoint andstream_ticketstable (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, empty204) no longer throws aSyntaxErrorthat masks the real HTTP failure (auth, admin and exceptions clients) - Locale-stable formatting: user-facing dates and numbers now use explicit
en-USformatting 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
setTimeoutis 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 againstNaN; 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
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 whateverprojectIdthe 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 sameverifyProjectAccessmembership 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/:idand/sigma-rules/:idreturned the channelconfigverbatim (including webhookauthtokens/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:
maskTextskipped 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_TTLoverride 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
nodemaileris bumped from^8.0.9to^9.0.1(GHSA-p6gq-j5cr-w38f, HIGH): the message-levelrawoption bypasseddisableFileAccess/disableUrlAccess, enabling arbitrary file read and full-response SSRF in the delivered message; our SMTP senders only use standardcreateTransport/sendMailfields and never passraw, but the dependency is patched regardless. The transitiveundici(pulled in only byjsdomin the frontend test toolchain) is pinned via the root pnpmoverridesto>=7.28.0 <8(GHSA-vmh5-mc38-953g HIGH, TLS certificate validation bypass via droppedrequestTlsin the SOCKS5ProxyAgent; GHSA-pr7r-676h-xcf6 MEDIUM, cross-user information disclosure via shared-cache whitespace bypass), resolving to7.28.0and staying on the 7.x linejsdom@29expects. No vulnerable version remains inpnpm-lock.yaml
Fixed
- ClickHouse "Query with id = ... is already running" under concurrent queries (#213 regression): the request-context propagation derived the ClickHouse
query_iddeterministically fromrequestId + operation, so two same-operation queries running concurrently within one request reused the same id and ClickHouse rejected the second. Thequery_idnow keeps the request id + operation as a readable prefix (correlation is also carried in the SQLlog_comment) and appends a random suffix so every query is unique. ClickHouse-only - Sigma condition operator precedence:
parseExpressionfolded AND/OR strictly left-to-right, soa or b and cevaluated 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:
upsertTraceis now race-free on TimescaleDB (single atomicINSERT ... 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_idcolumn (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:
deleteMetricsByTimeRangeonly deletes the matched metrics' exemplars (was all exemplars in the window); the metrics overview sorts before$last; metadatadistinctno longer drops numeric/boolean values;ingestReturningreturns 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
minBaselineValueof 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_statsums 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;
getTopServiceshonors thetobound - 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
deadstate (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|containschains) 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
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 requireprojectIdwith an explicitGLOBAL_SCOPEsentinel. - 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 anX-Logtide-Event-Version: 1header. - OTLP log metadata shape: resource attributes now land under
metadata.resource; structured bodies preserved undermetadata['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
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
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
What's Changed
- scheduled email digest by @DEVAANSH001 in #209
- Request context propagation across HTTP, jobs, and DB layer (#213) by @Polliog in #222
- Tenant data isolation audit (#219) by @Polliog in #228
- tenant isolation follow-ups: migration prefix collision + 404 semantics by @Polliog in #229
- resource usage metering (#212) by @Polliog in #237
New Contributors
- @DEVAANSH001 made their first contribution in #209
Full Changelog: v0.9.7...v1.0.0b-beta1
v0.9.7
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.tsguard and wired it into the HTTP/TCP monitors and theWebhookProvider, but the actual alert/Sigma webhook delivery path was left on the old inline filter.sendWebhookNotificationinqueue/jobs/alert-notification.ts(reached by thealert-notificationsBullMQ worker for threshold, rate-of-change and Sigma-rule alerts) still ran a barefetch(webhook_url, …)guarded only by a string-basedisPrivateIP(). That check was bypassable: any non-dotted-quad hostname returnedfalse(no DNS resolution), so a domain whose A record points at127.0.0.1/169.254.169.254/ an internal host passed the filter and the resolved internal address was then connected; the barefetchused the defaultredirect: 'follow', so a public host that302s to an internal URL was followed straight there; and CGNAT (100.64.0.0/10), IPv6, IPv4-mapped IPv6,0.0.0.0/8and198.18.0.0/15were 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:sendWebhookNotificationnow routes delivery throughsafeFetch(url, init, { allowPrivate: config.MONITOR_ALLOW_PRIVATE_TARGETS }), exactly asWebhookProviderdoes (DNS resolution + per-redirect-hop revalidation + full IPv4/IPv6 private/reserved range coverage), mappingSsrfBlockedErrorto the existing "private/internal addresses are not allowed" error. The inlineisPrivateIP/BLOCKED_HOSTSfilter and the barefetchare removed, and blocked targets are now rejected before the response body is read (closing the read-back oracle). TheMONITOR_ALLOW_PRIVATE_TARGETSopt-in still lets self-hosted deployments target internal endpoints. Reported by tonghuaroot
Full Changelog: v0.9.6...v0.9.7
v0.9.6
Changed
- Frontend now runs as a full SPA on
adapter-nodeinstead of doing SSR with client hydration. A singleexport const ssr = falsein the new root+layout.tscascades 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/callbackpage and the various dashboard subpages, each previously patched with its own per-routessr = 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.tsfiles 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 overrideexport const ssr = truein that page's+page.tsputs it back on SSR without affecting anything else) - Removed 22 redundant route-level
ssr = falsedeclarations (dashboard/+layout.ts,dashboard/admin/+layout.tsand 20+page.tsfiles 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/tracesand/dashboard/metricswhen 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 atsearch/+page.svelte:441-444(and the identical pattern attraces/+page.svelte:142-145andmetrics/+page.svelte:90-93) readconst logsProjectIds = availability?.logsand then branched onlogsProjectIds ? filter : fallback. WhengetProjectDataAvailabilitylegitimately 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, andhasLoadedOncestayedfalseso theSkeletonTablerendered forever. Fix: guard the truthy branch withlogsProjectIds && logsProjectIds.length > 0so 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 toON DELETE CASCADEonlogs.project_id(tracked separately under the soft-delete projects epic) - OIDC login failed against issuer-identifying providers (e.g. Authelia) because the
isscallback parameter was dropped (#233, #234): the OIDC callback handler extracted onlycodeandstatefrom 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) appendissto the redirect andopenid-client'sauthorizationCodeGrant()validates it, so the token exchange failed with "issuer parameter missing" and login never completed. The Fastify callback route now forwards the fullrequest.querythroughhandleOidcCallbackinto the provider, which replays every parameter onto the callback URL handed to the token exchange (soiss,session_state, etc. survive); the requiredcode/stateare always re-asserted from the validated values. Duplicated/array-valued query params are collapsed to a single value withsearchParams.setinstead of being appended, since OIDC authorization-response params are single-valued per RFC 6749 / RFC 9207, avoiding a malformed URL with duplicateiss/codereachingauthorizationCodeGrant. 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 anorganizationIdand aprojectId, 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 ownorganizationId(membership check passes) with a victim's knownprojectIdand reach another tenant's data. Confirmed on four handlers:POST /api/v1/alerts/previewreturnedsampleLogs(time, service, level, message, trace ID) from the foreign project;POST /api/v1/alertsandPOST /api/v1/monitorslet a rule/monitor be scoped to a foreign project (the monitor case also surfaces on the victim's public status page, which renders byproject_id); andGET/DELETE /api/v1/sourcemapslet a member list or delete another tenant's source maps. Fix: a sharedprojectsService.projectBelongsToOrg(projectId, organizationId)helper (singleprojectslookup filtered on bothidandorganization_id) is now enforced right after the existing membership check on each of those routes, returning403when the project is foreign. The alert/monitor update paths don't accept aprojectIdand the custom-dashboards panel pipeline already had an equivalentensureProjectInOrgguard 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.tsthen calledfetch(target, { redirect: 'follow' })andcreateConnection({ host, port }), so a registered user could point a monitor athttp://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.254cloud metadata), CGNAT (100.64.0.0/10), multicast and reserved IPv4/IPv6 ranges (including IPv4-mapped IPv6 and ULA/fc00::/7, link-localfe80::/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 ofredirect: 'follow'. The guard runs both at monitor create/update time (immediate400feedback) and at execution time (authoritative, returns ablockedresult). Private/internal targets are denied by default; self-hosted deployments that legitimately monitor internal services can opt back in withMONITOR_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
- @outcast292 made their first contribution in #233
Full Changelog: v0.9.5...v0.9.6
v0.9.5
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 readparams.metadataFilters, so any log search or alert rule that relied on ametadata.*filter came back unfiltered on those engines (the filter appeared to do nothing). ClickHouse now translates each filter to a predicate over the JSONmetadatacolumn usingJSONExtractStringpaired withJSONHasto distinguish a missing key from an empty string (not_in/not_equalssplit on whether the key is present,containsusespositionCaseInsensitive). MongoDB builds one clause per filter keyed onmetadata.<key>, all wrapped in$andso repeated filters on the same key don't overwrite each other, withinclude_missingcontrolling whethernot_equals/not_inalso match documents where the field is absent ($exists). Covered by new per-engine translator tests - Error notifications spammed one email per occurrence:
processErrorNotificationsent an in-app notification, email and webhook for every exception occurrence, suppressed only when the error group's status wasignored. A high-frequency client error (e.g. a Svelteeffect_update_depth_exceededloop 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 viaUPDATE 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 viaERROR_NOTIFICATION_COOLDOWN_MINUTES(default 15, set0to notify on every occurrence). Occurrence counts on the error group and in-app dashboards are unaffected monitoringrejected by the notification-channels defaults endpoint:PUT/GET /api/v1/notification-channels/defaults/:eventTypevalidated:eventTypeagainst a local Zod enum of['alert', 'sigma', 'incident', 'error']that was missingmonitoring, even though the sharedNotificationEventTypetype, the DBorganization_default_channelsconstraint (migration 037) and the service all support it. Setting a default monitoring channel returned400 Validation error("received 'monitoring'"). Addedmonitoringto the route enum so the five event types are consistent across the stack
Added
- Migration 043
error_notification_throttle: adds a nullablelast_notified_at TIMESTAMPTZcolumn toerror_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.protobufjs→7.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).kysely→0.28.17(bounded>=0.28.17 <0.29; JSON-path traversal injection inJSONPathBuilder.key()/.at()).svelte→5.55.9(>=5.55.7; SSR XSS via spread attributes and promise serialization, DOM-clobbering XSS,<svelte:element>ReDoS).@sveltejs/kit→2.61.1(>=2.60.1;query.batchcross-talk).fast-uri→3.1.2(path traversal + host confusion via percent-encoded segments).qs→6.15.2(DoS inqs.stringify),devalue→5.8.1(sparse-array DoS),brace-expansion→5.0.6(numeric-range DoS),ws→8.21.0(uninitialized memory disclosure).protobufjsandkyselywere deliberately held on their current major/minor lines (their "latest" is a breaking jump) while still landing on the patched release