CVE-2021-32662 (published 2021-06-03T22:15Z)

Backstage is an open platform for building developer portals, and techdocs-common contains common functionalities for Backstage's TechDocs. In `@backstage/techdocs-common` versions prior to 0.6.3, a malicious actor could read sensitive files from the environment where TechDocs documentation is built and published by setting a particular path for `docs_dir` in `mkdocs.yml`. These files would then be available over the TechDocs backend API. This vulnerability is mitigated by the fact that an attacker would need access to modify the `mkdocs.yml` in the documentation source code, and would also need access to the TechDocs backend API. The vulnerability is patched in the `0.6.3` release of `@backstage/techdocs-common`.

Severity: LOW
CVSS v2 base score: 3.5 (AV:N/AC:M/Au:S/C:P/I:N/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/backstage/backstage
commit_details: repo_url: https://github.com/backstage/backstage
author: Jussi Hallila
author_date: 2021-05-20 14:03:42+02:00
msg: Add validation to prevent docs_dir from being an absolute path

* Adds a new validation function to helpers to prevent generation when mkdocs.yml is not present or is invalid
* Handles vulnerability where docs_dir can be put in as an absolute path which copies and exposes the files from that absolute path in the file system

Signed-off-by: Jussi Hallila <jussi@hallila.com>
repository: repo_url: https://github.com/backstage/backstage
repo_name: backstage/backstage
description: Backstage is an open framework for building developer portals
repo_language: TypeScript
owner: backstage
file_changes: filename: gold-kings-jog.md
change_type: ModificationType.ADD
diff: @@ -0,0 +1,5 @@
+---
+'@backstage/techdocs-common': patch
+---
+
+Adding validation to mkdocs.yml parsing to prevent directory tree traversing

programming_language: Markdown
filename: mkdocs_invalid_doc_dir.yml
change_type: ModificationType.ADD
diff: @@ -0,0 +1,3 @@
+site_name: Test site name
+site_description: Test site description
+docs_dir: /etc

programming_language: SQL
filename: helpers.test.ts
change_type: ModificationType.MODIFY
diff: @@ -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();
+    });
+  });
 });

programming_language: TypeScript
filename: helpers.ts
change_type: ModificationType.MODIFY
diff: @@ -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.
  *

programming_language: TypeScript
filename: techdocs.ts
change_type: ModificationType.MODIFY
diff: @@ -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',

programming_language: TypeScript