Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ All notable changes to LogTide will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.2] - 2026-02-03

### Security

- **Fastify Security Vulnerabilities**: Upgraded Fastify from 4.x to 5.7.3+ to fix critical CVEs
- CVE: Content-Type header tab character allows body validation bypass (fixed in 5.7.2)
- CVE: DoS via Unbounded Memory Allocation in sendWebStream (fixed in 5.7.3)
- Updated all @fastify/* plugins to compatible v5 versions

### Fixed

- **API Batch Request Limit**: Fixed `logIds must NOT have more than 100 items` error in log search tail mode
- `getLogIdentifiersBatch` now automatically splits requests into batches of 100
- Supports up to 1000 logs in tail mode without errors
- Batches executed in parallel for performance

- **Unicode Escape Sequences**: Fixed `unsupported Unicode escape sequence` error during log ingestion
- Sanitizes `\u0000` (null characters) from log data before PostgreSQL insertion
- Affects message, service, metadata, trace_id, and span_id fields

- **POST Requests Without Body**: Fixed CDN/proxy compatibility issues with empty POST requests
- `disablePack`: Now sends `organizationId` in request body instead of query string
- `notification-channels/test`: Now sends `organizationId` in request body
- `resendInvitation`, `testConnection`, `leaveOrganization`: Now send empty `{}` body
- Backend routes accept `organizationId` from body or query for backwards compatibility

---

## [0.5.1] - 2026-02-01

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<a href="https://codecov.io/gh/logtide-dev/logtide"><img src="https://codecov.io/gh/logtide-dev/logtide/branch/main/graph/badge.svg" alt="Coverage"></a>
<a href="https://hub.docker.com/r/logtide/backend"><img src="https://img.shields.io/docker/v/logtide/backend?label=docker&logo=docker" alt="Docker"></a>
<a href="https://artifacthub.io/packages/helm/logtide/logtide"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/logtide" alt="Artifact Hub"></a>
<img src="https://img.shields.io/badge/version-0.5.1-blue.svg" alt="Version">
<img src="https://img.shields.io/badge/version-0.5.2-blue.svg" alt="Version">
<img src="https://img.shields.io/badge/license-AGPLv3-blue.svg" alt="License">
<img src="https://img.shields.io/badge/status-alpha-orange.svg" alt="Status">
<img src="https://img.shields.io/badge/cloud-free_during_alpha-success.svg" alt="Free Cloud">
Expand Down Expand Up @@ -136,7 +136,7 @@ Total control over your data. **No build required** - uses pre-built images from

**Docker Images:** [Docker Hub](https://hub.docker.com/r/logtide/backend) | [GitHub Container Registry](https://github.com/logtide-dev/logtide/pkgs/container/logtide-backend)

> **Production:** Pin versions with `LOGTIDE_BACKEND_IMAGE=logtide/backend:0.5.1` in your `.env` file.
> **Production:** Pin versions with `LOGTIDE_BACKEND_IMAGE=logtide/backend:0.5.2` in your `.env` file.

> **ARM64 / Raspberry Pi:** LogTide images support `linux/amd64` and `linux/arm64`. For Fluent Bit on ARM64, set `FLUENT_BIT_IMAGE=cr.fluentbit.io/fluent/fluent-bit:4.2.2` in your `.env` file.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@logtide/root",
"version": "0.5.1",
"version": "0.5.2",
"private": true,
"description": "LogTide - Self-hosted log management platform",
"author": "LogTide Team",
Expand Down
16 changes: 8 additions & 8 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@logtide/backend",
"version": "0.5.1",
"version": "0.5.2",
"private": true,
"description": "LogTide Backend API",
"type": "module",
Expand Down Expand Up @@ -35,20 +35,20 @@
"seed:load-test": "tsx src/scripts/seed-load-test.ts"
},
"dependencies": {
"@fastify/cors": "^9.0.1",
"@fastify/env": "^4.4.0",
"@fastify/helmet": "^11.1.1",
"@fastify/rate-limit": "^9.1.0",
"@fastify/websocket": "^10.0.1",
"@fastify/cors": "^10.0.2",
"@fastify/env": "^5.0.1",
"@fastify/helmet": "^13.0.1",
"@fastify/rate-limit": "^10.2.1",
"@fastify/websocket": "^11.0.2",
"@logtide/sdk-node": "^0.1.0",
"@logtide/shared": "workspace:*",
"@maxmind/geoip2-node": "^6.3.4",
"@opentelemetry/otlp-transformer": "^0.208.0",
"bcrypt": "^6.0.0",
"bullmq": "^5.65.1",
"dotenv": "^17.2.3",
"fastify": "^4.29.1",
"fastify-plugin": "^4.5.1",
"fastify": "^5.7.3",
"fastify-plugin": "^5.0.1",
"graphile-worker": "^0.16.6",
"ioredis": "^5.8.2",
"js-yaml": "^4.1.1",
Expand Down
6 changes: 3 additions & 3 deletions packages/backend/src/modules/auth/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ declare module 'fastify' {
*/
const authPlugin: FastifyPluginAsync = async (fastify) => {
fastify.decorateRequest('authenticated', false);
fastify.decorateRequest('projectId', null);
fastify.decorateRequest('organizationId', null);
fastify.decorateRequest('projectId', undefined);
fastify.decorateRequest('organizationId', undefined);

fastify.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => {
// Skip auth for public routes and session-based auth routes
Expand Down Expand Up @@ -121,5 +121,5 @@ const authPlugin: FastifyPluginAsync = async (fastify) => {

export default fp(authPlugin, {
name: 'auth',
fastify: '4.x',
fastify: '5.x',
});
5 changes: 3 additions & 2 deletions packages/backend/src/modules/detection-packs/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,12 @@ export async function detectionPacksRoutes(fastify: FastifyInstance) {
fastify.post('/:packId/disable', async (request: any, reply) => {
try {
const { packId } = packIdSchema.parse(request.params);
const organizationId = request.query.organizationId as string;
// Accept from body (preferred) or query (legacy)
const organizationId = (request.body?.organizationId || request.query.organizationId) as string;

if (!organizationId) {
return reply.status(400).send({
error: 'organizationId query parameter is required',
error: 'organizationId is required',
});
}

Expand Down
34 changes: 33 additions & 1 deletion packages/backend/src/modules/ingestion/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,15 @@ const ingestionRoutes: FastifyPluginAsync = async (fastify) => {

// Override default JSON parser to handle NDJSON disguised as application/json
// Fluent Bit sometimes sends json_lines with application/json content-type
fastify.removeContentTypeParser('application/json');
fastify.addContentTypeParser('application/json', { parseAs: 'string' }, (_req, body, done) => {
try {
const bodyStr = body.toString().trim();
const bodyStr = body?.toString()?.trim() || '';
if (!bodyStr) {
// Empty body - return empty object (Fastify 5 compatibility)
done(null, {});
return;
}
// Check if it looks like NDJSON (multiple lines, each starting with {)
const lines = bodyStr.split('\n').filter(line => line.trim());
if (lines.length > 1 && lines.every(line => line.trim().startsWith('{'))) {
Expand Down Expand Up @@ -225,6 +231,19 @@ const ingestionRoutes: FastifyPluginAsync = async (fastify) => {
timestamp: { type: 'string' },
},
},
400: {
type: 'object',
properties: {
error: { type: 'string' },
details: { type: 'object' },
},
},
401: {
type: 'object',
properties: {
error: { type: 'string' },
},
},
},
},
handler: async (request: any, reply) => {
Expand Down Expand Up @@ -305,6 +324,19 @@ const ingestionRoutes: FastifyPluginAsync = async (fastify) => {
timestamp: { type: 'string' },
},
},
400: {
type: 'object',
properties: {
error: { type: 'string' },
details: { type: 'object' },
},
},
401: {
type: 'object',
properties: {
error: { type: 'string' },
},
},
},
},
handler: async (request: any, reply) => {
Expand Down
33 changes: 27 additions & 6 deletions packages/backend/src/modules/ingestion/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ import { CacheManager } from '../../utils/cache.js';
import { notificationPublisher } from '../streaming/index.js';
import { correlationService, type IdentifierMatch } from '../correlation/service.js';

/**
* Remove null characters (\u0000) that PostgreSQL doesn't support in text fields.
*/
function sanitizeForPostgres<T>(value: T): T {
if (value === null || value === undefined) return value;
if (typeof value === 'string') {
return value.replace(/\u0000/g, '') as T;
}
if (Array.isArray(value)) {
return value.map(sanitizeForPostgres) as T;
}
if (typeof value === 'object') {
const result: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value)) {
result[k] = sanitizeForPostgres(v);
}
return result as T;
}
return value;
}

export class IngestionService {
/**
* Ingest logs in batch
Expand Down Expand Up @@ -40,16 +61,16 @@ export class IngestionService {
}
}

// Convert logs to database format
// Convert logs to database format (sanitize to remove \u0000 which PostgreSQL doesn't support)
const dbLogs = logs.map((log) => ({
time: typeof log.time === 'string' ? new Date(log.time) : log.time,
project_id: projectId,
service: log.service,
service: sanitizeForPostgres(log.service),
level: log.level,
message: log.message,
metadata: log.metadata || null,
trace_id: log.trace_id || null,
span_id: (log as { span_id?: string }).span_id || null,
message: sanitizeForPostgres(log.message),
metadata: sanitizeForPostgres(log.metadata) || null,
trace_id: sanitizeForPostgres(log.trace_id) || null,
span_id: sanitizeForPostgres((log as { span_id?: string }).span_id) || null,
}));

// Insert logs in batch and return IDs
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/modules/notification-channels/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,8 @@ export async function notificationChannelsRoutes(fastify: FastifyInstance) {
fastify.post('/:id/test', async (request: any, reply) => {
try {
const { id } = channelIdSchema.parse(request.params);
const organizationId = request.query.organizationId as string;
// Accept from body (preferred) or query (legacy)
const organizationId = (request.body?.organizationId || request.query.organizationId) as string;

if (!organizationId) {
return reply.status(400).send({ error: 'organizationId is required' });
Expand Down
12 changes: 12 additions & 0 deletions packages/backend/src/modules/sigma/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ export async function sigmaRoutes(fastify: FastifyInstance) {
},
},
response: {
403: {
type: 'object',
properties: {
error: { type: 'string' },
},
},
404: {
type: 'object',
properties: {
Expand Down Expand Up @@ -347,6 +353,12 @@ export async function sigmaRoutes(fastify: FastifyInstance) {
success: { type: 'boolean' },
},
},
403: {
type: 'object',
properties: {
error: { type: 'string' },
},
},
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/plugins/internal-logging-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,6 @@ const internalLoggingPlugin: FastifyPluginCallback = (fastify, _options, done) =
};

export default fp(internalLoggingPlugin, {
fastify: '4.x',
fastify: '5.x',
name: 'internal-logging-plugin',
});
22 changes: 21 additions & 1 deletion packages/backend/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ export async function build(opts = {}) {
...opts,
});

// Override default JSON parser to allow empty bodies (Fastify 5 breaking change)
// This is needed because some routes may receive requests with Content-Type: application/json
// but empty body (e.g., POST requests without body from some clients)
fastify.removeContentTypeParser('application/json');
fastify.addContentTypeParser('application/json', { parseAs: 'string' }, (_req, body, done) => {
try {
const bodyStr = body?.toString()?.trim() || '';
if (!bodyStr) {
// Empty body - return empty object
done(null, {});
} else {
done(null, JSON.parse(bodyStr));
}
} catch (err: any) {
const error = new Error(`Invalid JSON: ${err.message}`);
(error as any).statusCode = 400;
done(error, undefined);
}
});

await fastify.register(cors, {
origin: true,
credentials: true,
Expand Down Expand Up @@ -88,7 +108,7 @@ export async function build(opts = {}) {
return {
status: 'ok',
timestamp: new Date().toISOString(),
version: '0.5.1',
version: '0.5.2',
};
});

Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/utils/internal-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export async function initializeInternalLogging(): Promise<void> {
globalMetadata: {
service: process.env.SERVICE_NAME || 'logtide-backend',
env: process.env.NODE_ENV || 'development',
version: process.env.npm_package_version || '0.5.1',
version: process.env.npm_package_version || '0.5.2',
hostname: process.env.HOSTNAME || 'unknown',
},
});
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@logtide/frontend",
"version": "0.5.1",
"version": "0.5.2",
"private": true,
"description": "LogTide Frontend Dashboard",
"type": "module",
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/src/lib/api/admin-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export class AdminAuthAPI {
async testConnection(id: string): Promise<TestConnectionResult> {
return this.request(`/admin/auth/providers/${id}/test`, {
method: 'POST',
body: JSON.stringify({}),
});
}

Expand Down
2 changes: 2 additions & 0 deletions packages/frontend/src/lib/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ export class AuthAPI {
const response = await fetch(`${getApiBaseUrl()}/auth/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({}),
});

if (!response.ok) {
Expand Down
35 changes: 27 additions & 8 deletions packages/frontend/src/lib/api/correlation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,35 @@ class CorrelationAPI {
return {};
}

const BATCH_SIZE = 100;
const url = `${getApiBaseUrl()}/logs/identifiers/batch`;
const response = await this.fetch<{
success: boolean;
data: { identifiers: Record<string, IdentifierMatch[]> };
}>(url, {
method: 'POST',
body: JSON.stringify({ logIds }),
});

return response.data.identifiers;
// Split into batches of 100 to respect API limits
const batches: string[][] = [];
for (let i = 0; i < logIds.length; i += BATCH_SIZE) {
batches.push(logIds.slice(i, i + BATCH_SIZE));
}

// Execute all batches in parallel
const results = await Promise.all(
batches.map((batch) =>
this.fetch<{
success: boolean;
data: { identifiers: Record<string, IdentifierMatch[]> };
}>(url, {
method: 'POST',
body: JSON.stringify({ logIds: batch }),
})
)
);

// Merge all results
const merged: Record<string, IdentifierMatch[]> = {};
for (const response of results) {
Object.assign(merged, response.data.identifiers);
}

return merged;
}
}

Expand Down
Loading
Loading