Skip to content

Commit 474b86f

Browse files
committed
fix(cbinsights): accept only plain decimal organization IDs
Number reads "0x10" as 16 and "1e2" as 100, so either notation resolved to a real but unintended organization and the request spent credits on it. Both the path-scoped and the bulk validators now require a plain run of digits, and use Number.isSafeInteger so an ID past the precision limit cannot round to a neighbouring one.
1 parent 5222387 commit 474b86f

2 files changed

Lines changed: 62 additions & 6 deletions

File tree

apps/sim/tools/cbinsights/cbinsights.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,40 @@ describe('cbinsights request building', () => {
313313
})
314314
})
315315

316+
/*
317+
* `Number` reads "0x10" as 16 and "1e2" as 100, so either notation would
318+
* resolve to a real but unintended organization and bill for it. An ID field
319+
* should only accept a plain run of digits.
320+
*/
321+
it.each(['0x10', '1e2', '12.5', 'true', '', ' '])(
322+
'rejects the alternate numeric notation %j in an ID list',
323+
async (entry) => {
324+
mockFetch([AUTH_OK])
325+
await expect(
326+
cbinsightsGetOrgOutlookTool.directExecution!({ ...CREDS, orgId: entry } as never)
327+
).rejects.toThrow(/"orgId" must be a positive integer/)
328+
}
329+
)
330+
331+
it('still accepts a plain decimal ID, with or without padding', async () => {
332+
mockFetch([AUTH_OK, { body: {} }, { body: {} }])
333+
await cbinsightsGetOrgOutlookTool.directExecution!({ ...CREDS, orgId: ' 129410 ' } as never)
334+
expect(calls[1].url).toContain('/organizations/129410/outlook')
335+
336+
await cbinsightsGetOrgOutlookTool.directExecution!({ ...CREDS, orgId: 129410 } as never)
337+
expect(calls[2].url).toContain('/organizations/129410/outlook')
338+
})
339+
340+
it('rejects an alternate numeric notation inside a bulk ID list', async () => {
341+
mockFetch([AUTH_OK])
342+
await expect(
343+
cbinsightsListFundingsTool.directExecution!({
344+
...CREDS,
345+
orgIds: '129410, 0x10',
346+
} as never)
347+
).rejects.toThrow(/must contain only positive integers \(invalid: 0x10\)/)
348+
})
349+
316350
it('rejects a non-integer organization ID rather than interpolating it into the path', async () => {
317351
mockFetch([AUTH_OK])
318352
await expect(

apps/sim/tools/cbinsights/utils.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,33 @@ export async function cbInsightsRequest<T>(
241241
}
242242
}
243243

244+
/**
245+
* Accepts only a plain run of decimal digits.
246+
*
247+
* `Number` is far more permissive than an ID field should be: it reads `0x10`
248+
* as 16 and `1e2` as 100, so a pasted value in either notation would resolve to
249+
* a real but unintended organization and the request would spend credits on it.
250+
*/
251+
const DECIMAL_ID = /^\d+$/
252+
253+
/** Coerces one entry to a positive integer ID, or returns null if it is not one. */
254+
function toOrgId(value: unknown): number | null {
255+
if (typeof value === 'number') {
256+
return Number.isInteger(value) && value > 0 ? value : null
257+
}
258+
if (typeof value !== 'string') return null
259+
260+
const trimmed = value.trim()
261+
if (!DECIMAL_ID.test(trimmed)) return null
262+
263+
const parsed = Number(trimmed)
264+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null
265+
}
266+
244267
/** Validates the organization ID a path-scoped endpoint interpolates. */
245268
export function requireOrgId(value: unknown): number {
246-
const parsed = typeof value === 'number' ? value : Number(String(value ?? '').trim())
247-
if (!Number.isInteger(parsed) || parsed <= 0) {
269+
const parsed = toOrgId(value)
270+
if (parsed === null) {
248271
throw new Error(
249272
`CB Insights "orgId" must be a positive integer (received "${String(value ?? '')}")`
250273
)
@@ -309,10 +332,9 @@ function toPositiveIntegers(entries: readonly unknown[], paramName: string): num
309332
const ids: number[] = []
310333

311334
for (const entry of entries) {
312-
const label = typeof entry === 'number' ? String(entry) : String(entry).trim()
313-
const parsed = typeof entry === 'number' ? entry : Number(label)
314-
if (!Number.isInteger(parsed) || parsed <= 0) {
315-
invalid.push(label)
335+
const parsed = toOrgId(entry)
336+
if (parsed === null) {
337+
invalid.push(typeof entry === 'string' ? entry.trim() : String(entry))
316338
continue
317339
}
318340
ids.push(parsed)

0 commit comments

Comments
 (0)