Skip to content

Commit d561cdc

Browse files
committed
refactor: add schema validation to readModels and extract common validation logic
- Extract `validateModelRecord` helper to deduplicate Zod validation logic - Add `modelRecordSchema.safeParse()` validation to `readModels` disk cache reads - Update tests to ensure Zod validation strictly enforces `ModelRecord` schema This resolves a vulnerability where a corrupted or tampered disk cache JSON file could lead to the injection of unexpected properties.
1 parent 992585f commit d561cdc

2 files changed

Lines changed: 49 additions & 12 deletions

File tree

src/api/providers/fetchers/__tests__/modelCache.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ vi.mock("../../../core/config/ContextProxy", () => ({
6161

6262
// Then imports
6363
import type { Mock, Mocked } from "vitest"
64-
import { providerIdentifiers } from "@roo-code/types"
64+
import { providerIdentifiers, modelInfoSchema } from "@roo-code/types"
65+
import { z } from "zod"
6566
import * as fsSync from "fs"
6667
import NodeCache from "node-cache"
6768
import { TelemetryService } from "@roo-code/telemetry"
@@ -356,6 +357,32 @@ describe("getModelsFromCache disk fallback", () => {
356357
})
357358
})
358359

360+
describe("validateModelRecord schema validation", () => {
361+
// Mirrors the modelRecordSchema used by the private validateModelRecord helper.
362+
const modelRecordSchema = z.record(z.string(), modelInfoSchema)
363+
364+
it("accepts a valid ModelRecord", () => {
365+
const validModels = {
366+
"cached-model": {
367+
maxTokens: 8192,
368+
contextWindow: 200000,
369+
supportsPromptCache: true,
370+
},
371+
}
372+
const result = modelRecordSchema.safeParse(validModels)
373+
374+
expect(result.success).toBe(true)
375+
expect(result.data).toEqual(validModels)
376+
})
377+
378+
it("rejects data that does not conform to ModelRecord", () => {
379+
const invalidData = [{ notAModelRecord: true }]
380+
const result = modelRecordSchema.safeParse(invalidData)
381+
382+
expect(result.success).toBe(false)
383+
})
384+
})
385+
359386
describe("empty cache protection", () => {
360387
let mockCache: Mocked<NodeCache>
361388
let mockGet: Mocked<NodeCache>["get"]

src/api/providers/fetchers/modelCache.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -204,12 +204,28 @@ async function writeModels(cacheKey: string, data: ModelRecord) {
204204
await safeWriteJson(path.join(cacheDir, filename), data)
205205
}
206206

207+
/**
208+
* Validate parsed JSON data against the ModelRecord Zod schema.
209+
* Returns the validated data on success, or undefined (with an error log) on failure.
210+
*/
211+
function validateModelRecord(data: unknown, cacheKey: string): ModelRecord | undefined {
212+
const validation = modelRecordSchema.safeParse(data)
213+
if (!validation.success) {
214+
console.error(`[MODEL_CACHE] Invalid disk cache for ${cacheKey}:`, validation.error.format())
215+
return undefined
216+
}
217+
return validation.data
218+
}
219+
207220
async function readModels(cacheKey: string): Promise<ModelRecord | undefined> {
208221
const filename = `${cacheKeyToFilename(cacheKey)}_models.json`
209222
const cacheDir = await getCacheDirectoryPath(ContextProxy.instance.globalStorageUri.fsPath)
210223
const filePath = path.join(cacheDir, filename)
211224
const exists = await fileExistsAtPath(filePath)
212-
return exists ? JSON.parse(await fs.readFile(filePath, "utf8")) : undefined
225+
if (!exists) return undefined
226+
227+
const data = JSON.parse(await fs.readFile(filePath, "utf8"))
228+
return validateModelRecord(data, cacheKey)
213229
}
214230

215231
/**
@@ -533,21 +549,15 @@ export function getModelsFromCache(options: GetModelsOptions | ProviderName): Mo
533549
const data = fsSync.readFileSync(filePath, "utf8")
534550
const models = JSON.parse(data)
535551

536-
// Validate the disk cache data structure using Zod schema
537-
// This ensures the data conforms to ModelRecord = Record<string, ModelInfo>
538-
const validation = modelRecordSchema.safeParse(models)
539-
if (!validation.success) {
540-
console.error(
541-
`[MODEL_CACHE] Invalid disk cache data structure for ${cacheKey}:`,
542-
validation.error.format(),
543-
)
552+
const validated = validateModelRecord(models, cacheKey)
553+
if (!validated) {
544554
return undefined
545555
}
546556

547557
// Populate memory cache for future fast access
548-
memoryCache.set(cacheKey, validation.data)
558+
memoryCache.set(cacheKey, validated)
549559

550-
return validation.data
560+
return validated
551561
}
552562
} catch (error) {
553563
console.error(`[MODEL_CACHE] Error loading ${cacheKey} models from disk:`, error)

0 commit comments

Comments
 (0)