Skip to content

Commit 6c7a12b

Browse files
authored
fix(websearch): reject non-positive WEB_CUSTOM env overrides (#2124)
The custom web-search provider read WEB_CUSTOM_TIMEOUT_SEC and WEB_CUSTOM_MAX_BODY_KB with Number(x) || DEFAULT. That idiom only rescues 0 and NaN — a negative or Infinity value is truthy and passes straight through. A negative WEB_CUSTOM_MAX_BODY_KB makes the "body exceeds N bytes" check reject every POST search, and a negative WEB_CUSTOM_TIMEOUT_SEC drives an immediate abort on every request. Route both through readPositiveEnvNumber, which falls back to the default for missing, empty, non-finite, or non-positive input.
1 parent f553d08 commit 6c7a12b

2 files changed

Lines changed: 56 additions & 3 deletions

File tree

src/tools/WebSearchTool/providers/custom.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import {
33
acquireSharedMutationLock,
44
releaseSharedMutationLock,
55
} from '../../../test/sharedMutationLock.js'
6-
import { extractHits, customProvider, isPrivateHostname } from './custom.js'
6+
import {
7+
extractHits,
8+
customProvider,
9+
isPrivateHostname,
10+
readPositiveEnvNumber,
11+
} from './custom.js'
712

813
async function importFreshCustomProvider() {
914
const stamp = `${Date.now()}-${Math.random()}`
@@ -420,3 +425,33 @@ describe('isPrivateHostname — IPv6', () => {
420425
expect(isPrivateHostname('not:an:ipv6')).toBe(false)
421426
})
422427
})
428+
429+
// ---------------------------------------------------------------------------
430+
// readPositiveEnvNumber — WEB_CUSTOM_TIMEOUT_SEC / WEB_CUSTOM_MAX_BODY_KB
431+
// ---------------------------------------------------------------------------
432+
433+
describe('readPositiveEnvNumber', () => {
434+
test('parses a valid positive override', () => {
435+
expect(readPositiveEnvNumber('45', 120)).toBe(45)
436+
expect(readPositiveEnvNumber('0.5', 120)).toBe(0.5)
437+
})
438+
439+
test('falls back for missing / empty / non-numeric input', () => {
440+
expect(readPositiveEnvNumber(undefined, 120)).toBe(120)
441+
expect(readPositiveEnvNumber('', 120)).toBe(120)
442+
expect(readPositiveEnvNumber('fast', 120)).toBe(120)
443+
})
444+
445+
test('falls back for zero and negative values instead of passing them through', () => {
446+
// The old `Number(x) || DEFAULT` idiom rescued 0 but let negatives past —
447+
// a negative timeout aborts every request and a negative body cap makes the
448+
// size guard reject every POST.
449+
expect(readPositiveEnvNumber('0', 120)).toBe(120)
450+
expect(readPositiveEnvNumber('-1', 120)).toBe(120)
451+
expect(readPositiveEnvNumber('-9999', 300)).toBe(300)
452+
})
453+
454+
test('falls back for non-finite values (Infinity)', () => {
455+
expect(readPositiveEnvNumber('1e999', 120)).toBe(120)
456+
})
457+
})

src/tools/WebSearchTool/providers/custom.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,24 @@ const DEFAULT_MAX_BODY_KB = 300
142142
/** Default request timeout in seconds. */
143143
const DEFAULT_TIMEOUT_SECONDS = 120
144144

145+
/**
146+
* Read a positive numeric env override, falling back to `fallback` for
147+
* missing, empty, non-finite, or non-positive values.
148+
*
149+
* `Number(raw) || fallback` alone only rescues 0 and NaN — a negative or
150+
* Infinity value is truthy and passes straight through. That silently breaks
151+
* the size/timeout guards downstream: a negative WEB_CUSTOM_MAX_BODY_KB makes
152+
* the "body exceeds N bytes" check fire for every POST, and a negative
153+
* WEB_CUSTOM_TIMEOUT_SEC aborts every request immediately.
154+
*/
155+
export function readPositiveEnvNumber(
156+
raw: string | undefined,
157+
fallback: number,
158+
): number {
159+
const parsed = Number(raw)
160+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
161+
}
162+
145163
/** Header names that are always allowed (case-insensitive). */
146164
const SAFE_HEADER_NAMES = new Set([
147165
'accept',
@@ -515,7 +533,7 @@ function buildRequest(query: string) {
515533
const bodyTemplate = process.env.WEB_BODY_TEMPLATE
516534
if (bodyTemplate) {
517535
const body = bodyTemplate.replace(/\{query\}/g, query)
518-
const maxBodyBytes = (Number(process.env.WEB_CUSTOM_MAX_BODY_KB) || DEFAULT_MAX_BODY_KB) * 1024
536+
const maxBodyBytes = readPositiveEnvNumber(process.env.WEB_CUSTOM_MAX_BODY_KB, DEFAULT_MAX_BODY_KB) * 1024
519537
if (Buffer.byteLength(body) > maxBodyBytes) {
520538
throw new Error(
521539
`POST body exceeds ${maxBodyBytes} bytes. ` +
@@ -582,7 +600,7 @@ export function extractHits(raw: any, jsonPath?: string): SearchHit[] {
582600
// ---------------------------------------------------------------------------
583601

584602
async function fetchWithRetry(url: string, init: RequestInit, signal?: AbortSignal): Promise<any> {
585-
const timeoutSec = Number(process.env.WEB_CUSTOM_TIMEOUT_SEC) || DEFAULT_TIMEOUT_SECONDS
603+
const timeoutSec = readPositiveEnvNumber(process.env.WEB_CUSTOM_TIMEOUT_SEC, DEFAULT_TIMEOUT_SECONDS)
586604
const timeoutMs = timeoutSec * 1000
587605
let lastErr: Error | undefined
588606
let lastStatus: number | undefined

0 commit comments

Comments
 (0)