From 5d36b961e32d744434eded1d73d2a8a3f198080c Mon Sep 17 00:00:00 2001 From: Ruslans Tarasovs Date: Thu, 30 Apr 2026 10:18:17 +0300 Subject: [PATCH] Validate that symlink to README.md does not escape the directory Signed-off-by: Ruslans Tarasovs --- .../src/stages/generate/helpers.test.ts | 42 +++++++++++++++++++ .../src/stages/generate/helpers.ts | 5 +++ 2 files changed, 47 insertions(+) diff --git a/plugins/techdocs-node/src/stages/generate/helpers.test.ts b/plugins/techdocs-node/src/stages/generate/helpers.test.ts index 1f198b43e9..511ba970d1 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.test.ts @@ -543,6 +543,48 @@ describe('helpers', () => { ], ]); }); + + it('should use a symlink to README.md if docs/index.md does not exist', async () => { + mockDir.setContent({ + 'information.md': 'information.md content', + 'README.md': ctx => ctx.symlink('./information.md'), + }); + + await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger }); + + await expect( + fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'), + ).resolves.toEqual('information.md content'); + expect(warn.mock.calls).toEqual([ + [`${path.normalize('docs/index.md')} not found.`], + [`${path.normalize('docs/README.md')} not found.`], + [`${path.normalize('docs/readme.md')} not found.`], + ]); + }); + + it('should reject a symlink pointing outside of the current directory', async () => { + const anotherMockDir = createMockDirectory(); + + mockDir.setContent({ + 'information.md': 'information.md content', + 'README.md': ctx => ctx.symlink(anotherMockDir.resolve('tmp/secret')), + }); + + anotherMockDir.setContent({ + tmp: { + secret: 'password', + }, + }); + + await expect( + patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger }), + ).rejects.toThrow(/not allowed to refer to a location outside/i); + expect(warn.mock.calls).toEqual([ + [`${path.normalize('docs/index.md')} not found.`], + [`${path.normalize('docs/README.md')} not found.`], + [`${path.normalize('docs/readme.md')} not found.`], + ]); + }); }); describe('addBuildTimestampMetadata', () => { diff --git a/plugins/techdocs-node/src/stages/generate/helpers.ts b/plugins/techdocs-node/src/stages/generate/helpers.ts index caecb47524..351fff2f93 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.ts @@ -399,6 +399,11 @@ export const patchIndexPreBuild = async ({ await fs.ensureDir(docsPath); for (const filePath of fallbacks) { + if (!isChildPath(inputDir, filePath)) { + throw new NotAllowedError( + `Path ${filePath} is not allowed to refer to a location outside ${inputDir}`, + ); + } try { await fs.copyFile(filePath, indexMdPath); return;