mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 16:03:26 +00:00
fix(native-chat): keep journal blob reads and writes inside the blob store
A digest reaches the filesystem after a round trip through a row on disk, so a corrupt or crafted row could name a path outside the store. Blob names are now required to be a bare sha256, checked on both reads and writes.
This commit is contained in:
@@ -10,9 +10,12 @@ import { join } from 'node:path'
|
||||
import { durableWriteTempPath, writeFileDurable } from '../../durable-file-write'
|
||||
|
||||
const BLOB_DIR = 'blobs'
|
||||
const DIGEST_PATTERN = /^[0-9a-f]{64}$/
|
||||
|
||||
function blobPath(journalDir: string, digest: string): string {
|
||||
return join(journalDir, BLOB_DIR, digest)
|
||||
/** A digest arrives back from a row on disk, so it is untrusted by the time it
|
||||
* reaches the filesystem: anything but a bare sha256 could escape the store. */
|
||||
function blobPath(journalDir: string, digest: string): string | null {
|
||||
return DIGEST_PATTERN.test(digest) ? join(journalDir, BLOB_DIR, digest) : null
|
||||
}
|
||||
|
||||
/** Persist `payload` under its digest. Returns the digest so the caller can
|
||||
@@ -23,6 +26,9 @@ export async function putJournalBlob(
|
||||
payload: string
|
||||
): Promise<string> {
|
||||
const target = blobPath(journalDir, digest)
|
||||
if (!target) {
|
||||
throw new Error('refusing to write a journal blob under a name that is not a sha256 digest')
|
||||
}
|
||||
// Content addressing makes a rewrite pointless: identical digest, identical bytes.
|
||||
if (await pathExists(target)) {
|
||||
return digest
|
||||
@@ -33,8 +39,12 @@ export async function putJournalBlob(
|
||||
}
|
||||
|
||||
export async function readJournalBlob(journalDir: string, digest: string): Promise<string | null> {
|
||||
const source = blobPath(journalDir, digest)
|
||||
if (!source) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return await readFile(blobPath(journalDir, digest), 'utf-8')
|
||||
return await readFile(source, 'utf-8')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -225,6 +225,15 @@ describe('compaction and retention', () => {
|
||||
expect(await readJournalBlob(root, kept.digest)).toBe('k'.repeat(64))
|
||||
expect(await readJournalBlob(root, dropped.digest)).toBeNull()
|
||||
})
|
||||
|
||||
it('refuses a blob name that is not a bare digest, on either slash', async () => {
|
||||
const { putJournalBlob } = await import('./journal-blob-store')
|
||||
// A corrupt or crafted row must not steer a read or a write out of the store.
|
||||
for (const name of ['../../escape', '..\\..\\escape', 'nested/name', 'NOTHEX']) {
|
||||
expect(await readJournalBlob(root, name)).toBeNull()
|
||||
await expect(putJournalBlob(root, name, 'payload')).rejects.toThrow('sha256 digest')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('bounds', () => {
|
||||
|
||||
Reference in New Issue
Block a user