Files
landing-builder/lib/exporter/verify.integrity.test.ts
T
jaybe 7928f9b6c5 feat: ZIP 경로 가드 추가 및 무결성 size 검증 도입 (TDD)
- buildZip에 경로 하드닝 추가: ../, 절대경로(/), 역슬래시(\) 금지
- assets.integrity.index.json에 size(byte) 필드 추가
- verifyIntegrity에서 size 불일치 검증 추가
- 경로/사이즈 검증 테스트 추가: zip.pathGuard.test.ts
- 전체 테스트 그린 유지
2025-11-16 17:30:47 +09:00

72 lines
3.4 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { AssetManager } from '@/lib/assets/assetManager'
import { buildAssetsIntegrityIndex } from '@/lib/exporter/assets.integrity.index'
import { verifyIntegrity } from '@/lib/exporter/verify'
function u8(n: number): Uint8Array { const a = new Uint8Array(n); for (let i=0;i<n;i++) a[i]=i&0xff; return a }
function td(b: Uint8Array){ return new TextDecoder().decode(b) }
function te(s: string){ return new TextEncoder().encode(s) }
describe('verifyIntegrity', () => {
it('returns ok=true for valid assets and integrity index', async () => {
const am = new AssetManager({ maxBytes: 1024*1024, allowedExts: ['png','txt'] })
const a = await am.add({ name: 'a.png', type: 'image/png', bytes: u8(5) })
const b = await am.add({ name: 'b.txt', type: 'text/plain', bytes: u8(7) })
const assets = am.toZipStructure()
const idx = buildAssetsIntegrityIndex(am)
const extras: Record<string, Uint8Array> = { 'assets.integrity.index.json': idx }
const res = verifyIntegrity(extras, assets)
expect(res.ok).toBe(true)
expect(res.errors.length).toBe(0)
// sanity: index contains both paths
const parsed = JSON.parse(td(idx)) as { entries: Array<{ path: string }> }
const paths = parsed.entries.map(e => e.path)
expect(paths).toContain(a.path)
expect(paths).toContain(b.path)
})
it('detects byte tampering in an asset', async () => {
const am = new AssetManager({ maxBytes: 1024*1024, allowedExts: ['png','txt'] })
await am.add({ name: 'a.png', type: 'image/png', bytes: u8(5) })
await am.add({ name: 'b.txt', type: 'text/plain', bytes: u8(7) })
const assets = am.toZipStructure()
const idx = buildAssetsIntegrityIndex(am)
// tamper one asset byte
const somePath = Object.keys(assets)[0]
const tampered = assets[somePath].slice()
tampered[0] = (tampered[0] ^ 0xff) & 0xff
assets[somePath] = tampered
const res = verifyIntegrity({ 'assets.integrity.index.json': idx }, assets)
expect(res.ok).toBe(false)
expect(res.errors.some((e: string) => e.includes(somePath) && e.toLowerCase().includes('mismatch'))).toBe(true)
})
it('detects missing entry or missing asset referenced by index', async () => {
const am = new AssetManager({ maxBytes: 1024*1024, allowedExts: ['png','txt'] })
await am.add({ name: 'a.png', type: 'image/png', bytes: u8(5) })
await am.add({ name: 'b.txt', type: 'text/plain', bytes: u8(7) })
const assets = am.toZipStructure()
const idxRaw = buildAssetsIntegrityIndex(am)
const obj = JSON.parse(td(idxRaw)) as { entries: Array<{ path: string; integrity: string }>, bundleHash: string }
// remove one entry
const removed = obj.entries.shift()!
const idx2 = te(JSON.stringify(obj))
const res1 = verifyIntegrity({ 'assets.integrity.index.json': idx2 }, assets)
expect(res1.ok).toBe(false)
expect(res1.errors.some((e: string) => e.toLowerCase().includes('bundlehash'))).toBe(true)
// reference a missing path in index
obj.entries.unshift({ path: 'assets/missing.bin', integrity: 'sha256-AAAA' })
const idx3 = te(JSON.stringify(obj))
const res2 = verifyIntegrity({ 'assets.integrity.index.json': idx3 }, assets)
expect(res2.ok).toBe(false)
expect(res2.errors.some((e: string) => e.includes('assets/missing.bin') && e.toLowerCase().includes('missing'))).toBe(true)
// keep lints happy
expect(removed).toBeTruthy()
})
})