v0.9.1
Released
Fix
Security Hardening, Input Validation & Bug Fixes
SQL injection fix in querySpans, ReDoS protection for monitor assertions, 25+ bug fixes across auth, pipelines, SIEM, monitoring, and SSR safety, plus comprehensive input validation on all route params.
- SQL injection fix in querySpans sortBy/sortOrder for both TimescaleDB and ClickHouse
- ReDoS protection on HTTP monitor body assertion via safe-regex2
- UUID validation, positive-int clamping, and max-length guards across all route params
- PII salt race condition, SSE duplicate sends, and Sigma sync FK corruption fixed
- localStorage SSR crash guards and URL-encoding fixes in frontend navigation
Fixed
- Identifier pattern update/delete failed with “Organization ID is required” (#193): PUT and DELETE routes required
organizationIdas an explicit query param or API key context, but session-based auth never sets that field. Now falls back to the user’s first organization, consistent with GET and POST - Project rename failed with “Expected string, received null” (#195):
updateProjectSchemausedz.string().optional()which rejectsnull, but projects without a description sendnullfrom the DB. Changed toz.string().nullable().optional()and updatedUpdateProjectInputaccordingly - Pipeline create/preview/import returned generic “Validation error” (#193, #194): POST routes expected
organizationIdin the request body but the frontend sends it as a query param. Routes now merge the query param into the body before Zod validation. Frontend error messages now surface the first validation detail instead of the generic label - Invitation accept race condition: wrapping the membership check + insert in a transaction caused the
accepted_atupdate to roll back when throwing “already a member”. Split the early-exit path out of the transaction and added 23505 unique-constraint handling for true concurrent accepts - SSE live tail duplicate sends:
latestLogpicked the oldest entry from a DESC-sorted array instead of the newest, causing every poll to re-fetch and re-send all logs since the oldest timestamp. Replaced with a defensive max-time computation - Sigma sync corrupted alert_rule_id FK: fallback
alertRuleId || existing.idwrote the sigma rule’s own PK into thealert_rulesFK column when no alert was auto-created. Now omits the column entirely unless a new alert rule was just created - Exception log viewer returned empty results for org-wide error groups:
getLogsForErrorGrouppassed an empty string asprojectIdto reservoir when no project filter was set. Now groups log IDs by their exception’sproject_idand issues onegetByIdscall per project - ReDoS in HTTP monitor body assertion: user-supplied regex pattern was compiled with only a 256-char length limit, no catastrophic-backtracking check. Added
safe-regex2validation and a compile-error catch - OTLP int64 precision loss:
parseInt()silently truncated int64 attribute values exceedingNumber.MAX_SAFE_INTEGER. Unsafe values are now kept as strings in metadata - PII salt race condition fallthrough: if two workers raced on the first hash for an org, the loser could return an unpersisted local salt when the retry read failed, permanently desynchronizing PII hashes. Now scopes the catch to 23505 and throws on unexpected errors
- Monitor status fallthrough on missing row:
processCheckResultsilently skipped the entire state machine (no status update, no notifications) whenmonitor_statuswas undefined. Now re-reads from DB or creates a default row before proceeding - Test isolation: auth mode pollution across test files:
system_settingswas never reset between tests, so any test settingauth.mode='none'caused 12 unrelated “401 without auth” assertions to return 503. Added cleanup + cache invalidation to globalbeforeEach - Sigma detection tests used
sigma_idafter code migrated toid: fixture data still passedrule.sigma_idassigmaRuleId, which would not match the new.where('id', '=', ...)queries translateDeletedropped level filter in reservoir:pushFilterreturn value was discarded for the level condition, corrupting$Nparameter slots when both service and level filters were present- SQL injection in
querySpansvia sortBy/sortOrder: user-controlled strings were interpolated directly into raw SQL in both TimescaleDB and ClickHouse engines. Added explicit column/direction allowlists ingestSpanshardcoded::uuid[]for project_id: ignored theprojectIdTypeengine option, breaking text-based project IDs with a Postgres cast errorlocalStorageSSR crash in organization store: 7 directlocalStoragecalls without a browser guard would throwReferenceErrorduring server-side rendering. Addedbrowsercheck on all accesses- Invite token not URL-encoded in redirect:
goto(/login?redirect=/invite/${token})corrupted the redirect path for tokens containing+,=, or/. Now wraps inencodeURIComponent ruleIdnot URL-encoded in security dashboard navigation: inconsistent withserviceNameandtechniquewhich already usedencodeURIComponent- Missing UUID validation on monitoring route params: all
/:idhandlers passedrequest.params.iddirectly to DB queries without format validation. Added Zod.uuid()parsing on every route - Negative limit/days in monitoring routes:
Number("-1") || 50evaluates to-1(truthy), passing a negative value toLIMIT. Replaced with aparsePositiveIntguard that clamps to[1, max] - Missing UUID validation on status-incident route params: same issue as monitoring -
:idparams were used unvalidated in DB queries - SIEM comment body has no max length:
z.string().min(1)with no upper bound allowed arbitrarily large comment payloads. Added.max(10000) - Notifications and alerts limit/offset NaN passthrough:
parseInt("abc")returnedNaN, which was passed to Kysely.limit(NaN). Added safe integer parsing with fallback and max cap - Correlation referenceTime accepted invalid date strings: schema validated only
{type: 'string'}, sonew Date("garbage")flowed into Kysely WHERE clauses asInvalid Date. Addedformat: 'date-time'and a defensive 400 response - Log pipeline comment contradicted jsonb merge direction: code comment said “do NOT overwrite existing” but the in-progress fix had flipped the
jsonb ||operand order so pipeline fields now win. Updated comment to match actual behavior