diff --git a/.changeset/gold-kings-jog.md b/.changeset/gold-kings-jog.md new file mode 100644 index 0000000000..9bc901e2af --- /dev/null +++ b/.changeset/gold-kings-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/techdocs-common': patch +--- + +Adding validation to mkdocs.yml parsing to prevent directory tree traversing diff --git a/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml new file mode 100644 index 0000000000..b3d87243c2 --- /dev/null +++ b/packages/techdocs-common/src/stages/generate/__fixtures__/mkdocs_invalid_doc_dir.yml @@ -0,0 +1,3 @@ +site_name: Test site name +site_description: Test site description +docs_dir: /etc diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 05402d0cf5..94a7560fba 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -27,6 +27,7 @@ import { isValidRepoUrlForMkdocs, patchMkdocsYmlPreBuild, storeEtagMetadata, + validateMkdocsYaml, } from './helpers'; const mockEntity = { @@ -43,6 +44,9 @@ const mkdocsYml = fs.readFileSync( const mkdocsYmlWithRepoUrl = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_repo_url.yml'), ); +const mkdocsYmlWithInvalidDocDir = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_invalid_doc_dir.yml'), +); const mockLogger = getVoidLogger(); const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; @@ -300,4 +304,28 @@ describe('helpers', () => { expect(json.etag).toBe('etag123abc'); }); }); + + describe('validateMkdocsYaml', () => { + beforeEach(() => { + mockFs({ + '/mkdocs.yml': mkdocsYml, + '/mkdocs_with_repo_url.yml': mkdocsYmlWithRepoUrl, + '/mkdocs_invalid_doc_dir.yml': mkdocsYmlWithInvalidDocDir, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should return true on when no docs_dir present', async () => { + await expect(validateMkdocsYaml('/mkdocs.yml')).resolves.toBeUndefined(); + }); + + it('should return false on absolute doc_dir path', async () => { + await expect( + validateMkdocsYaml('/mkdocs_invalid_doc_dir.yml'), + ).rejects.toThrow(); + }); + }); }); diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index 88f42c1e4e..51771603de 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -16,6 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { spawn } from 'child_process'; +import { isAbsolute, normalize } from 'path'; import fs from 'fs-extra'; import yaml from 'js-yaml'; import { PassThrough, Writable } from 'stream'; @@ -138,6 +139,30 @@ export const getRepoUrlFromLocationAnnotation = ( return undefined; }; +/** + * Validating mkdocs config file for incorrect/insecure values + * Throws on invalid configs + * + * @param {string} mkdocsYmlPath Absolute path to mkdocs.yml or equivalent of a docs site + */ +export const validateMkdocsYaml = async (mkdocsYmlPath: string) => { + let mkdocsYmlFileString; + try { + mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); + } catch (error) { + throw new Error( + `Could not read MkDocs YAML config file ${mkdocsYmlPath} before for validation: ${error.message}`, + ); + } + + const mkdocsYml: any = yaml.load(mkdocsYmlFileString); + if (mkdocsYml.docs_dir && isAbsolute(normalize(mkdocsYml.docs_dir))) { + throw new Error( + "docs_dir configuration value in mkdocs can't be an absolute directory path for security reasons. Use relative paths instead which are resolved relative to your mkdocs.yml file location.", + ); + } +}; + /** * Update the mkdocs.yml file before TechDocs generator uses it to generate docs site. * diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index 1a6b0a581c..1f2a9ae3ff 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -24,6 +24,7 @@ import { patchMkdocsYmlPreBuild, runCommand, storeEtagMetadata, + validateMkdocsYaml, } from './helpers'; import { GeneratorBase, GeneratorRunOptions } from './types'; @@ -79,14 +80,17 @@ export class TechdocsGenerator implements GeneratorBase { // TODO: In future mkdocs.yml can be mkdocs.yaml. So, use a config variable here to find out // the correct file name. // Do some updates to mkdocs.yml before generating docs e.g. adding repo_url + const mkdocsYmlPath = path.join(inputDir, 'mkdocs.yml'); if (parsedLocationAnnotation) { await patchMkdocsYmlPreBuild( - path.join(inputDir, 'mkdocs.yml'), + mkdocsYmlPath, this.logger, parsedLocationAnnotation, ); } + await validateMkdocsYaml(mkdocsYmlPath); + // Directories to bind on container const mountDirs = { [inputDir]: '/input',