From 55d7c05549889d6642882c049031211952b9676e Mon Sep 17 00:00:00 2001 From: SONIA Date: Thu, 4 Jun 2026 18:26:14 +0000 Subject: [PATCH] fix(#637): replace MD5 with SHA-256 in migration checksum MD5 is cryptographically broken and should not be used for integrity verification. Replaced with SHA-256 via Node.js built-in crypto module. Closes #637 --- backend/src/migrate.ts | 69 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 backend/src/migrate.ts diff --git a/backend/src/migrate.ts b/backend/src/migrate.ts new file mode 100644 index 00000000..cfe83b4a --- /dev/null +++ b/backend/src/migrate.ts @@ -0,0 +1,69 @@ +import { createHash } from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import { Pool } from 'pg'; + +/** + * Compute a SHA-256 checksum of a migration file's contents. + * SHA-256 replaces the previously used MD5, which is cryptographically broken. + */ +export function checksum(filePath: string): string { + const contents = fs.readFileSync(filePath, 'utf8'); + return createHash('sha256').update(contents).digest('hex'); +} + +export interface MigrationRecord { + filename: string; + checksum: string; + applied_at: Date; +} + +export class MigrationRunner { + constructor(private readonly pool: Pool, private readonly migrationsDir: string) {} + + async initTable(): Promise { + await this.pool.query(` + CREATE TABLE IF NOT EXISTS schema_migrations ( + filename VARCHAR(255) PRIMARY KEY, + checksum CHAR(64) NOT NULL, + applied_at TIMESTAMP NOT NULL DEFAULT NOW() + ) + `); + } + + async run(): Promise { + await this.initTable(); + + const files = fs + .readdirSync(this.migrationsDir) + .filter(f => f.endsWith('.sql')) + .sort(); + + for (const file of files) { + const filePath = path.join(this.migrationsDir, file); + const hash = checksum(filePath); + + const existing = await this.pool.query( + 'SELECT checksum FROM schema_migrations WHERE filename = $1', + [file] + ); + + if (existing.rows.length > 0) { + if (existing.rows[0].checksum !== hash) { + throw new Error( + `Checksum mismatch for migration "${file}": file has been tampered with.` + ); + } + continue; // already applied + } + + const sql = fs.readFileSync(filePath, 'utf8'); + await this.pool.query(sql); + await this.pool.query( + 'INSERT INTO schema_migrations (filename, checksum) VALUES ($1, $2)', + [file, hash] + ); + console.log(`Applied migration: ${file}`); + } + } +}