fix(security): Resolve 21 SQL injection vulnerabilities and add transactions
Security fixes (v1.2.2): - Fix SQL injection in analytics.ts (16 occurrences) - Fix SQL injection in advanced-search.ts (1 occurrence) - Fix SQL injection in search-queries.ts (1 occurrence) - Add validateDaysInterval(), isValidISODate(), validatePeriod() to security.ts - Use make_interval(days => N) for safe PostgreSQL intervals - Validate UUIDs BEFORE string construction Transaction support: - bulk-operations.ts: 6 atomic operations with withTransaction() - desk-sync.ts: 2 operations with transactions - export-import.ts: 1 operation with transaction Rate limiting: - Add automatic cleanup of expired entries (every 5 minutes) Audit: - Archive previous audit docs to docs/audits/2026-01-31-v1.2.1/ - Create new AUDIT-REQUEST.md for v1.2.2 verification Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,28 @@
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
|
||||
import { Pool } from 'pg';
|
||||
import { Pool, PoolClient } from 'pg';
|
||||
import { BaseTool, ToolResponse } from '../types/tools.js';
|
||||
import { isValidUUID } from '../utils/security.js';
|
||||
|
||||
/**
|
||||
* Execute operations within a transaction
|
||||
*/
|
||||
async function withTransaction<T>(pool: Pool, callback: (client: PoolClient) => Promise<T>): Promise<T> {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const result = await callback(client);
|
||||
await client.query('COMMIT');
|
||||
return result;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* bulk.archive_documents - Archive multiple documents
|
||||
*/
|
||||
@@ -30,12 +48,15 @@ const bulkArchiveDocuments: BaseTool<{ document_ids: string[] }> = {
|
||||
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
||||
}
|
||||
|
||||
const result = await pgClient.query(`
|
||||
UPDATE documents
|
||||
SET "archivedAt" = NOW(), "updatedAt" = NOW()
|
||||
WHERE id = ANY($1) AND "archivedAt" IS NULL AND "deletedAt" IS NULL
|
||||
RETURNING id, title
|
||||
`, [args.document_ids]);
|
||||
// Use transaction for atomic operation
|
||||
const result = await withTransaction(pgClient, async (client) => {
|
||||
return await client.query(`
|
||||
UPDATE documents
|
||||
SET "archivedAt" = NOW(), "updatedAt" = NOW()
|
||||
WHERE id = ANY($1) AND "archivedAt" IS NULL AND "deletedAt" IS NULL
|
||||
RETURNING id, title
|
||||
`, [args.document_ids]);
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
@@ -69,15 +90,18 @@ const bulkDeleteDocuments: BaseTool<{ document_ids: string[] }> = {
|
||||
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
||||
}
|
||||
|
||||
const deletedById = await pgClient.query(`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`);
|
||||
const userId = deletedById.rows.length > 0 ? deletedById.rows[0].id : null;
|
||||
// Use transaction for atomic operation
|
||||
const result = await withTransaction(pgClient, async (client) => {
|
||||
const deletedById = await client.query(`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`);
|
||||
const userId = deletedById.rows.length > 0 ? deletedById.rows[0].id : null;
|
||||
|
||||
const result = await pgClient.query(`
|
||||
UPDATE documents
|
||||
SET "deletedAt" = NOW(), "deletedById" = $2, "updatedAt" = NOW()
|
||||
WHERE id = ANY($1) AND "deletedAt" IS NULL
|
||||
RETURNING id, title
|
||||
`, [args.document_ids, userId]);
|
||||
return await client.query(`
|
||||
UPDATE documents
|
||||
SET "deletedAt" = NOW(), "deletedById" = $2, "updatedAt" = NOW()
|
||||
WHERE id = ANY($1) AND "deletedAt" IS NULL
|
||||
RETURNING id, title
|
||||
`, [args.document_ids, userId]);
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
@@ -115,19 +139,22 @@ const bulkMoveDocuments: BaseTool<{ document_ids: string[]; collection_id: strin
|
||||
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
||||
}
|
||||
|
||||
// Verify collection exists
|
||||
const collectionCheck = await pgClient.query(
|
||||
`SELECT id FROM collections WHERE id = $1 AND "deletedAt" IS NULL`,
|
||||
[args.collection_id]
|
||||
);
|
||||
if (collectionCheck.rows.length === 0) throw new Error('Collection not found');
|
||||
// Use transaction for atomic operation
|
||||
const result = await withTransaction(pgClient, async (client) => {
|
||||
// Verify collection exists
|
||||
const collectionCheck = await client.query(
|
||||
`SELECT id FROM collections WHERE id = $1 AND "deletedAt" IS NULL`,
|
||||
[args.collection_id]
|
||||
);
|
||||
if (collectionCheck.rows.length === 0) throw new Error('Collection not found');
|
||||
|
||||
const result = await pgClient.query(`
|
||||
UPDATE documents
|
||||
SET "collectionId" = $2, "parentDocumentId" = $3, "updatedAt" = NOW()
|
||||
WHERE id = ANY($1) AND "deletedAt" IS NULL
|
||||
RETURNING id, title, "collectionId"
|
||||
`, [args.document_ids, args.collection_id, args.parent_document_id || null]);
|
||||
return await client.query(`
|
||||
UPDATE documents
|
||||
SET "collectionId" = $2, "parentDocumentId" = $3, "updatedAt" = NOW()
|
||||
WHERE id = ANY($1) AND "deletedAt" IS NULL
|
||||
RETURNING id, title, "collectionId"
|
||||
`, [args.document_ids, args.collection_id, args.parent_document_id || null]);
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
@@ -161,12 +188,15 @@ const bulkRestoreDocuments: BaseTool<{ document_ids: string[] }> = {
|
||||
if (!isValidUUID(id)) throw new Error(`Invalid document ID: ${id}`);
|
||||
}
|
||||
|
||||
const result = await pgClient.query(`
|
||||
UPDATE documents
|
||||
SET "deletedAt" = NULL, "deletedById" = NULL, "updatedAt" = NOW()
|
||||
WHERE id = ANY($1) AND "deletedAt" IS NOT NULL
|
||||
RETURNING id, title
|
||||
`, [args.document_ids]);
|
||||
// Use transaction for atomic operation
|
||||
const result = await withTransaction(pgClient, async (client) => {
|
||||
return await client.query(`
|
||||
UPDATE documents
|
||||
SET "deletedAt" = NULL, "deletedById" = NULL, "updatedAt" = NOW()
|
||||
WHERE id = ANY($1) AND "deletedAt" IS NOT NULL
|
||||
RETURNING id, title
|
||||
`, [args.document_ids]);
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
@@ -204,29 +234,35 @@ const bulkAddUsersToCollection: BaseTool<{ user_ids: string[]; collection_id: st
|
||||
}
|
||||
|
||||
const permission = args.permission || 'read_write';
|
||||
const creatorResult = await pgClient.query(`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`);
|
||||
const createdById = creatorResult.rows.length > 0 ? creatorResult.rows[0].id : args.user_ids[0];
|
||||
|
||||
const added: string[] = [];
|
||||
const skipped: string[] = [];
|
||||
// Use transaction for atomic operation
|
||||
const { added, skipped } = await withTransaction(pgClient, async (client) => {
|
||||
const creatorResult = await client.query(`SELECT id FROM users WHERE role = 'admin' AND "deletedAt" IS NULL LIMIT 1`);
|
||||
const createdById = creatorResult.rows.length > 0 ? creatorResult.rows[0].id : args.user_ids[0];
|
||||
|
||||
for (const userId of args.user_ids) {
|
||||
// Check if already exists
|
||||
const existing = await pgClient.query(
|
||||
`SELECT "userId" FROM collection_users WHERE "userId" = $1 AND "collectionId" = $2`,
|
||||
[userId, args.collection_id]
|
||||
);
|
||||
const addedList: string[] = [];
|
||||
const skippedList: string[] = [];
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
skipped.push(userId);
|
||||
} else {
|
||||
await pgClient.query(`
|
||||
INSERT INTO collection_users ("userId", "collectionId", permission, "createdById", "createdAt", "updatedAt")
|
||||
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||
`, [userId, args.collection_id, permission, createdById]);
|
||||
added.push(userId);
|
||||
for (const userId of args.user_ids) {
|
||||
// Check if already exists
|
||||
const existing = await client.query(
|
||||
`SELECT "userId" FROM collection_users WHERE "userId" = $1 AND "collectionId" = $2`,
|
||||
[userId, args.collection_id]
|
||||
);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
skippedList.push(userId);
|
||||
} else {
|
||||
await client.query(`
|
||||
INSERT INTO collection_users ("userId", "collectionId", permission, "createdById", "createdAt", "updatedAt")
|
||||
VALUES ($1, $2, $3, $4, NOW(), NOW())
|
||||
`, [userId, args.collection_id, permission, createdById]);
|
||||
addedList.push(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { added: addedList, skipped: skippedList };
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
@@ -264,11 +300,14 @@ const bulkRemoveUsersFromCollection: BaseTool<{ user_ids: string[]; collection_i
|
||||
if (!isValidUUID(id)) throw new Error(`Invalid user ID: ${id}`);
|
||||
}
|
||||
|
||||
const result = await pgClient.query(`
|
||||
DELETE FROM collection_users
|
||||
WHERE "userId" = ANY($1) AND "collectionId" = $2
|
||||
RETURNING "userId"
|
||||
`, [args.user_ids, args.collection_id]);
|
||||
// Use transaction for atomic operation
|
||||
const result = await withTransaction(pgClient, async (client) => {
|
||||
return await client.query(`
|
||||
DELETE FROM collection_users
|
||||
WHERE "userId" = ANY($1) AND "collectionId" = $2
|
||||
RETURNING "userId"
|
||||
`, [args.user_ids, args.collection_id]);
|
||||
});
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user