diff --git a/scripts/lib/migration.mjs b/scripts/lib/migration.mjs index 8a5af51..3fa5603 100644 --- a/scripts/lib/migration.mjs +++ b/scripts/lib/migration.mjs @@ -5,6 +5,10 @@ import { join } from 'node:path'; const SOURCE_COUNT = 29; const DOCS_ROOT = 'src/content/docs/'; +function hasParentTraversal(path) { + return path.split('/').includes('..'); +} + export async function loadManifest(path) { return JSON.parse(await readFile(path, 'utf8')); } @@ -36,10 +40,10 @@ export function validateManifest(manifest) { if (source.startsWith('pages/wiki/')) { throw new Error(`Generic DokuWiki source is excluded: ${source}`); } - if (!(source === 'pages/start.txt' || source === 'pages/study.txt' || source.startsWith('pages/study/'))) { + if (hasParentTraversal(source) || !(source === 'pages/start.txt' || source === 'pages/study.txt' || source.startsWith('pages/study/'))) { throw new Error(`Source is outside the selected migration scope: ${source}`); } - if (!destination.startsWith(DOCS_ROOT)) { + if (hasParentTraversal(destination) || !destination.startsWith(DOCS_ROOT)) { throw new Error(`Destination is outside ${DOCS_ROOT}: ${destination}`); } if (sources.has(source)) { diff --git a/tests/migration-manifest.test.mjs b/tests/migration-manifest.test.mjs index 4f2a39c..5e7095a 100644 --- a/tests/migration-manifest.test.mjs +++ b/tests/migration-manifest.test.mjs @@ -17,6 +17,20 @@ test('generic DokuWiki pages are excluded', async () => { assert.equal(manifest.entries.some(({ source }) => source.startsWith('pages/wiki/')), false); }); +test('validation rejects source traversal outside the selected scope', async () => { + const manifest = await loadManifest('docs/migration-manifest.json'); + manifest.entries[2].source = 'pages/study/../../wiki/x.txt'; + + assert.throws(() => validateManifest(manifest), /outside the selected migration scope/); +}); + +test('validation rejects destination traversal outside the docs root', async () => { + const manifest = await loadManifest('docs/migration-manifest.json'); + manifest.entries[2].destination = 'src/content/docs/../../../outside.md'; + + assert.throws(() => validateManifest(manifest), /outside src\/content\/docs/); +}); + test('validation rejects duplicate destinations unless the later entry is a merge', async () => { const manifest = await loadManifest('docs/migration-manifest.json'); manifest.entries[1].mode = 'page';