allowed keys implementation
Signed-off-by: Bond Yan <bondy@spotify.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
'@backstage/plugin-techdocs-node': patch
|
||||
---
|
||||
|
||||
Added `techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys` configuration option to explicit bypass MkDocs configuration key restrictions. This enables support for additional MkDocs configuration keys beyond the default safe allowlist, such as the `hooks` key which some MkDocs plugins require.
|
||||
Vendored
+12
@@ -64,6 +64,18 @@ export interface Config {
|
||||
* List of mkdocs plugins which should be added as default to all mkdocs.yml files.
|
||||
*/
|
||||
defaultPlugins?: string[];
|
||||
|
||||
/**
|
||||
* List of additional MkDocs configuration keys to allow beyond
|
||||
* the default safe allowlist. This can introduce security vulnerabilities.
|
||||
*
|
||||
* WARNING: Some MkDocs configuration keys can execute arbitrary code. For example, the
|
||||
* 'hooks' key allows running arbitrary Python code during documentation generation.
|
||||
* Only use this in trusted environments where all mkdocs.yml files are audited.
|
||||
*
|
||||
* @see https://www.mkdocs.org/user-guide/configuration/#hooks
|
||||
*/
|
||||
dangerouslyAllowAdditionalKeys?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -910,6 +910,83 @@ another_unknown: true
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('hooks'));
|
||||
});
|
||||
|
||||
it('should allow additional keys when configured via dangerouslyAllowAdditionalKeys', async () => {
|
||||
const mkdocsWithHooksAndCustom = `site_name: Test
|
||||
hooks:
|
||||
- hook.py
|
||||
custom_dir: custom
|
||||
some_unknown_key: value
|
||||
`;
|
||||
mockDir.setContent({
|
||||
'mkdocs_allowed.yml': mkdocsWithHooksAndCustom,
|
||||
});
|
||||
|
||||
// Allow 'hooks' and 'custom_dir' but not 'some_unknown_key'
|
||||
await sanitizeMkdocsYml(
|
||||
mockDir.resolve('mkdocs_allowed.yml'),
|
||||
mockLogger,
|
||||
['hooks', 'custom_dir'],
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
mockDir.resolve('mkdocs_allowed.yml'),
|
||||
);
|
||||
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
// 'hooks' and 'custom_dir' should be preserved
|
||||
expect(parsedYml.hooks).toEqual(['hook.py']);
|
||||
expect(parsedYml.custom_dir).toBe('custom');
|
||||
// 'some_unknown_key' should still be removed
|
||||
expect(parsedYml.some_unknown_key).toBeUndefined();
|
||||
expect(parsedYml.site_name).toBe('Test');
|
||||
|
||||
// Should warn about the dangerous configuration AND the removed key
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'DANGEROUS: Allowing additional MkDocs configuration keys beyond the default safe allowlist: hooks, custom_dir',
|
||||
),
|
||||
);
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Removed the following unsupported configuration keys from mkdocs.yml: some_unknown_key',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn about dangerous keys even when no keys are removed', async () => {
|
||||
mockDir.setContent({
|
||||
'mkdocs_with_hooks.yml': mkdocsYmlWithHooks,
|
||||
});
|
||||
|
||||
await sanitizeMkdocsYml(
|
||||
mockDir.resolve('mkdocs_with_hooks.yml'),
|
||||
mockLogger,
|
||||
['hooks'],
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
mockDir.resolve('mkdocs_with_hooks.yml'),
|
||||
);
|
||||
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
|
||||
hooks?: string[];
|
||||
site_name: string;
|
||||
};
|
||||
|
||||
// Hooks should be preserved
|
||||
expect(parsedYml.hooks).toBeDefined();
|
||||
expect(parsedYml.site_name).toBe('Test site name');
|
||||
|
||||
// Should warn about dangerous configuration
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'DANGEROUS: Allowing additional MkDocs configuration keys beyond the default safe allowlist: hooks',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateDocsDirectory', () => {
|
||||
|
||||
@@ -198,15 +198,28 @@ export const patchMkdocsYmlWithPlugins = async (
|
||||
*
|
||||
* @param mkdocsYmlPath - Absolute path to mkdocs.yml or equivalent of a docs site
|
||||
* @param logger - A logger instance
|
||||
* @param additionalAllowedKeys - Optional array of additional keys to allow beyond the default allowlist
|
||||
*/
|
||||
export const sanitizeMkdocsYml = async (
|
||||
mkdocsYmlPath: string,
|
||||
logger: LoggerService,
|
||||
additionalAllowedKeys?: string[],
|
||||
) => {
|
||||
await patchMkdocsFile(mkdocsYmlPath, logger, mkdocsYml => {
|
||||
// Combine default allowed keys with additional keys
|
||||
const allowedKeys = new Set(ALLOWED_MKDOCS_KEYS);
|
||||
if (additionalAllowedKeys && additionalAllowedKeys.length > 0) {
|
||||
logger.warn(
|
||||
`DANGEROUS: Allowing additional MkDocs configuration keys beyond the default safe allowlist: ${additionalAllowedKeys.join(
|
||||
', ',
|
||||
)}. This may introduce security vulnerabilities. Only use in trusted environments.`,
|
||||
);
|
||||
additionalAllowedKeys.forEach(key => allowedKeys.add(key));
|
||||
}
|
||||
|
||||
// Identify keys that will be removed for logging
|
||||
const removedKeys = Object.keys(mkdocsYml).filter(
|
||||
key => !ALLOWED_MKDOCS_KEYS.has(key),
|
||||
key => !allowedKeys.has(key),
|
||||
);
|
||||
|
||||
if (removedKeys.length > 0) {
|
||||
@@ -220,7 +233,7 @@ export const sanitizeMkdocsYml = async (
|
||||
|
||||
// Build a new object with only allowed keys
|
||||
const sanitized: Record<string, unknown> = {};
|
||||
for (const key of ALLOWED_MKDOCS_KEYS) {
|
||||
for (const key of allowedKeys) {
|
||||
if (key in mkdocsYml) {
|
||||
sanitized[key] = (mkdocsYml as Record<string, unknown>)[key];
|
||||
}
|
||||
|
||||
@@ -115,7 +115,11 @@ export class TechdocsGenerator implements GeneratorBase {
|
||||
const docsDir = await validateMkdocsYaml(inputDir, content);
|
||||
|
||||
// Remove unsupported configuration keys
|
||||
await sanitizeMkdocsYml(mkdocsYmlPath, childLogger);
|
||||
await sanitizeMkdocsYml(
|
||||
mkdocsYmlPath,
|
||||
childLogger,
|
||||
this.options.dangerouslyAllowAdditionalKeys,
|
||||
);
|
||||
|
||||
// Validate that no symlinks in the docs directory point outside the input directory
|
||||
// This prevents path traversal attacks where malicious symlinks could leak host files
|
||||
@@ -257,5 +261,8 @@ export function readGeneratorConfig(
|
||||
defaultPlugins: config.getOptionalStringArray(
|
||||
'techdocs.generator.mkdocs.defaultPlugins',
|
||||
),
|
||||
dangerouslyAllowAdditionalKeys: config.getOptionalStringArray(
|
||||
'techdocs.generator.mkdocs.dangerouslyAllowAdditionalKeys',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export type GeneratorConfig = {
|
||||
omitTechdocsCoreMkdocsPlugin?: boolean;
|
||||
legacyCopyReadmeMdToIndexMd?: boolean;
|
||||
defaultPlugins?: string[];
|
||||
dangerouslyAllowAdditionalKeys?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user