fix: resolve 5 critical vulnerabilities from L3 security audit

fix: resolve 5 critical vulnerabilities from L3 security audit
This commit is contained in:
Renato Alcara
2026-02-11 00:32:13 -03:00
committed by GitHub
8 changed files with 95 additions and 45 deletions
+13 -28
View File
@@ -1,36 +1,12 @@
import { DEFAULT_SESSION_CLEANUP_CONFIG } from '../Defaults'
import type { SignalKeyStoreWithTransaction } from '../Types'
import type { SessionCleanupConfig, SessionCleanupStats, SignalKeyStoreWithTransaction } from '../Types'
import type { ILogger } from '../Utils/logger'
import { jidDecode } from '../WABinary'
import type { LIDMappingStore } from './lid-mapping'
import type { SessionActivityTracker } from './session-activity-tracker'
/**
* Session cleanup statistics
*/
export interface SessionCleanupStats {
totalScanned: number
secondaryDevicesDeleted: number
primaryDevicesDeleted: number
lidOrphansDeleted: number
totalDeleted: number
durationMs: number
errors: number
}
/**
* Session cleanup configuration
*/
export interface SessionCleanupConfig {
enabled: boolean
intervalMs: number
cleanupHour: number
secondaryDeviceInactiveDays: number
primaryDeviceInactiveDays: number
lidOrphanHours: number
cleanupOnStartup: boolean
autoCleanCorrupted: boolean
}
// Re-export for backward compatibility
export type { SessionCleanupConfig, SessionCleanupStats }
/**
* Session metadata for cleanup decisions
@@ -76,6 +52,7 @@ export const makeSessionCleanup = (
let initialTimeout: ReturnType<typeof setTimeout> | null = null
let lastCleanupAt: number = 0
let cleanupRunning: boolean = false
let startupCleanupPromise: Promise<void> | null = null
/**
* Get all sessions from database
@@ -399,8 +376,10 @@ export const makeSessionCleanup = (
// Run immediate cleanup on startup if enabled
if (config.cleanupOnStartup) {
logger.info('🚀 Running cleanup on startup...')
runCleanup().catch(err => {
startupCleanupPromise = runCleanup().catch(err => {
logger.error({ err }, 'Cleanup on startup failed')
}).then(() => {
startupCleanupPromise = null // Clear after completion
})
}
@@ -414,6 +393,12 @@ export const makeSessionCleanup = (
initialTimeout = setTimeout(async () => {
initialTimeout = null // Clear reference after execution
// Wait for startup cleanup to complete (if still running)
if (startupCleanupPromise) {
logger.debug('Waiting for startup cleanup to complete before scheduled cleanup...')
await startupCleanupPromise
}
// Run first cleanup
await runCleanup()
+6 -3
View File
@@ -3,7 +3,7 @@ import { Boom } from '@hapi/boom'
import { randomBytes } from 'crypto'
import Long from 'long'
import { proto } from '../../WAProto/index.js'
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, STATUS_EXPIRY_SECONDS } from '../Defaults'
import { DEFAULT_CACHE_TTLS, DEFAULT_SESSION_CLEANUP_CONFIG, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, STATUS_EXPIRY_SECONDS } from '../Defaults'
import { metrics, recordMessageReceived, recordHistorySyncMessages, recordMessageRetry, recordMessageFailure } from '../Utils/prometheus-metrics.js'
import type {
GroupParticipant,
@@ -76,8 +76,11 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
getMessage,
shouldIgnoreJid,
enableAutoSessionRecreation,
enableCTWARecovery
enableCTWARecovery,
sessionCleanupConfig
} = config
// Use nullish coalescing to handle partial config properly
const autoCleanCorrupted = sessionCleanupConfig?.autoCleanCorrupted ?? DEFAULT_SESSION_CLEANUP_CONFIG.autoCleanCorrupted
const sock = makeMessagesSocket(config)
const {
ev,
@@ -1227,7 +1230,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
category,
author,
decrypt
} = decryptMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '', signalRepository, logger)
} = decryptMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '', signalRepository, logger, autoCleanCorrupted)
const alt = msg.key.participantAlt || msg.key.remoteJidAlt
// Handle LID/PN mappings with hybrid approach:
+6 -1
View File
@@ -506,12 +506,17 @@ export const makeSocket = (config: SocketConfig) => {
const sessionActivityTracker = makeSessionActivityTracker(keys, logger)
// Session cleanup manager - removes inactive/orphaned sessions
// Merge user config with defaults to prevent partial overrides from breaking cleanup
const sessionCleanupConfig = {
...DEFAULT_SESSION_CLEANUP_CONFIG,
...(config.sessionCleanupConfig || {})
}
const sessionCleanup = makeSessionCleanup(
keys,
signalRepository.lidMapping,
sessionActivityTracker,
logger,
DEFAULT_SESSION_CLEANUP_CONFIG
sessionCleanupConfig
)
let lastDateRecv: Date
+26
View File
@@ -0,0 +1,26 @@
/**
* Session cleanup configuration
*/
export interface SessionCleanupConfig {
enabled: boolean
intervalMs: number
cleanupHour: number
secondaryDeviceInactiveDays: number
primaryDeviceInactiveDays: number
lidOrphanHours: number
cleanupOnStartup: boolean
autoCleanCorrupted: boolean
}
/**
* Session cleanup statistics
*/
export interface SessionCleanupStats {
totalScanned: number
secondaryDevicesDeleted: number
primaryDevicesDeleted: number
lidOrphansDeleted: number
totalDeleted: number
durationMs: number
errors: number
}
+4
View File
@@ -3,6 +3,7 @@ import type { URL } from 'url'
import { proto } from '../../WAProto/index.js'
import type { ILogger } from '../Utils/logger'
import type { CircuitBreakerOptions } from '../Utils/circuit-breaker'
import type { SessionCleanupConfig } from './SessionCleanup'
import type { AuthenticationState, LIDMapping, SignalAuthState, TransactionCapabilityOptions } from './Auth'
import type { GroupMetadata } from './GroupMetadata'
import { type MediaConnInfo, type WAMessageKey } from './Message'
@@ -252,4 +253,7 @@ export type SocketConfig = {
* @see https://github.com/WhiskeySockets/Baileys/pull/2294
*/
enableUnifiedSession?: boolean
/** Session cleanup configuration (optional, partial overrides merged with defaults) */
sessionCleanupConfig?: Partial<SessionCleanupConfig>
}
+1
View File
@@ -10,6 +10,7 @@ export * from './Product'
export * from './Call'
export * from './Signal'
export * from './Newsletter'
export * from './SessionCleanup'
import type { AuthenticationState } from './Auth'
import type { SocketConfig } from './Socket'
+18 -6
View File
@@ -1,6 +1,5 @@
import { Boom } from '@hapi/boom'
import { proto } from '../../WAProto/index.js'
import { DEFAULT_SESSION_CLEANUP_CONFIG } from '../Defaults'
import type { WAMessage, WAMessageKey } from '../Types'
import type { SignalRepositoryWithLIDStore } from '../Types/Signal'
import {
@@ -269,7 +268,8 @@ export const decryptMessageNode = (
meId: string,
meLid: string,
repository: SignalRepositoryWithLIDStore,
logger: ILogger
logger: ILogger,
autoCleanCorrupted: boolean = true
) => {
const { fullMessage, author, sender } = decodeMessageNode(stanza, meId, meLid)
return {
@@ -407,7 +407,7 @@ export const decryptMessageNode = (
)
// Automatic cleanup of corrupted session (if enabled)
if (DEFAULT_SESSION_CLEANUP_CONFIG.autoCleanCorrupted) {
if (autoCleanCorrupted) {
try {
await cleanupCorruptedSession(decryptionJid, repository, logger)
logger.info(
@@ -475,9 +475,21 @@ async function cleanupCorruptedSession(
// Build list of JIDs to delete (primary + secondary devices)
const jidsToDelete: string[] = []
// Determine if this is a LID or PN
const isLID = jid.endsWith('@lid')
const domain = isLID ? 'lid' : 's.whatsapp.net'
// Determine domain type correctly (handle hosted JIDs)
// JID formats:
// - PN: user@s.whatsapp.net
// - LID: user@lid
// - Hosted PN: user@hosted
// - Hosted LID: user@hosted.lid
const isLID = jid.endsWith('@lid') || jid.endsWith('@hosted.lid')
const isHosted = jid.includes('@hosted')
let domain: string
if (isLID) {
domain = isHosted ? 'hosted.lid' : 'lid'
} else {
domain = isHosted ? 'hosted' : 's.whatsapp.net'
}
// Primary device (0)
jidsToDelete.push(`${user}@${domain}`)
+21 -7
View File
@@ -37,7 +37,9 @@ describe('SessionCleanup', () => {
cleanupHour: 3,
secondaryDeviceInactiveDays: 15,
primaryDeviceInactiveDays: 30,
lidOrphanHours: 24
lidOrphanHours: 24,
cleanupOnStartup: false,
autoCleanCorrupted: false
}
it('should delete LID orphan after 24h of inactivity', async () => {
@@ -168,7 +170,9 @@ describe('SessionCleanup', () => {
cleanupHour: 3,
secondaryDeviceInactiveDays: 15,
primaryDeviceInactiveDays: 30,
lidOrphanHours: 24
lidOrphanHours: 24,
cleanupOnStartup: false,
autoCleanCorrupted: false
}
it('should delete secondary device (Web/Desktop) after 15 days', async () => {
@@ -257,7 +261,9 @@ describe('SessionCleanup', () => {
cleanupHour: 3,
secondaryDeviceInactiveDays: 15,
primaryDeviceInactiveDays: 30,
lidOrphanHours: 24
lidOrphanHours: 24,
cleanupOnStartup: false,
autoCleanCorrupted: false
}
it('should delete primary device after 30 days', async () => {
@@ -323,7 +329,9 @@ describe('SessionCleanup', () => {
cleanupHour: 3,
secondaryDeviceInactiveDays: 15,
primaryDeviceInactiveDays: 30,
lidOrphanHours: 24
lidOrphanHours: 24,
cleanupOnStartup: false,
autoCleanCorrupted: false
}
it('should handle exactly 24h for LID orphan (boundary)', async () => {
@@ -408,7 +416,9 @@ describe('SessionCleanup', () => {
cleanupHour: 3,
secondaryDeviceInactiveDays: 15,
primaryDeviceInactiveDays: 30,
lidOrphanHours: 24
lidOrphanHours: 24,
cleanupOnStartup: false,
autoCleanCorrupted: false
}
it('should delete multiple sessions of different types', async () => {
@@ -462,7 +472,9 @@ describe('SessionCleanup', () => {
cleanupHour: 3,
secondaryDeviceInactiveDays: 15,
primaryDeviceInactiveDays: 30,
lidOrphanHours: 24
lidOrphanHours: 24,
cleanupOnStartup: false,
autoCleanCorrupted: false
}
// @ts-ignore
@@ -491,7 +503,9 @@ describe('SessionCleanup', () => {
cleanupHour: 3,
secondaryDeviceInactiveDays: 5, // Custom: 5 days
primaryDeviceInactiveDays: 10, // Custom: 10 days
lidOrphanHours: 12 // Custom: 12 hours
lidOrphanHours: 12, // Custom: 12 hours
cleanupOnStartup: false,
autoCleanCorrupted: false
}
const now = Date.now()