Merge pull request #33370 from rtar/master
fix(plugin-techdocs-node): Move docs directory validation to after copying README.md
This commit is contained in:
@@ -657,6 +657,119 @@ theme:
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(['README.md', 'readme.md', 'docs/README.md', 'docs/readme.md'])(
|
||||
'should use a symlink to %s if docs/index.md does not exist',
|
||||
async fileName => {
|
||||
mockDir.setContent({
|
||||
'information.md': 'information.md content',
|
||||
[fileName]: ctx => ctx.symlink(mockDir.resolve('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');
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['README.md', 'readme.md', 'docs/README.md', 'docs/readme.md'])(
|
||||
'should reject a symlink from %s to outside of the current directory',
|
||||
async fileName => {
|
||||
const anotherMockDir = createMockDirectory();
|
||||
|
||||
mockDir.setContent({
|
||||
'information.md': 'information.md content',
|
||||
[fileName]: ctx => ctx.symlink(anotherMockDir.resolve('tmp/secret')),
|
||||
});
|
||||
|
||||
anotherMockDir.setContent({
|
||||
tmp: {
|
||||
secret: 'password',
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger }),
|
||||
).rejects.toThrow(
|
||||
/Source path .* is not allowed to refer to a location outside/i,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['README.md', 'readme.md', 'docs/README.md', 'docs/readme.md'])(
|
||||
'should write %s to a symlink docs directory if docs/index.md does not exist',
|
||||
async fileName => {
|
||||
mockDir.setContent({
|
||||
'target/docs': {},
|
||||
docs: ctx => ctx.symlink('./target/docs'),
|
||||
[fileName]: `${fileName} content`,
|
||||
});
|
||||
|
||||
await patchIndexPreBuild({
|
||||
inputDir: mockDir.path,
|
||||
logger: mockLogger,
|
||||
});
|
||||
|
||||
await expect(
|
||||
fs.readFile(mockDir.resolve('target/docs/index.md'), 'utf-8'),
|
||||
).resolves.toEqual(`${fileName} content`);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['README.md', 'readme.md'])(
|
||||
'should reject creating docs dir if target symlink points to non-existing directory outside of the current directory',
|
||||
async fileName => {
|
||||
const anotherMockDir = createMockDirectory();
|
||||
|
||||
mockDir.setContent({
|
||||
docs: ctx => ctx.symlink(anotherMockDir.resolve('docs')),
|
||||
'information.md': 'information.md content',
|
||||
[fileName]: `${fileName} content`,
|
||||
});
|
||||
|
||||
await expect(
|
||||
patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger }),
|
||||
).rejects.toThrow(
|
||||
/Target path .* is not allowed to refer to a location outside/i,
|
||||
);
|
||||
|
||||
await expect(fs.exists(anotherMockDir.resolve('docs'))).resolves.toBe(
|
||||
false,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['README.md', 'readme.md'])(
|
||||
'should reject creating docs dir if target symlink points to existing directory outside of the current directory',
|
||||
async fileName => {
|
||||
const anotherMockDir = createMockDirectory();
|
||||
|
||||
mockDir.setContent({
|
||||
docs: ctx => ctx.symlink(anotherMockDir.resolve('docs')),
|
||||
'information.md': 'information.md content',
|
||||
[fileName]: `${fileName} content`,
|
||||
});
|
||||
|
||||
anotherMockDir.setContent({
|
||||
docs: {},
|
||||
});
|
||||
|
||||
await expect(
|
||||
patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger }),
|
||||
).rejects.toThrow(
|
||||
/Target path .* is not allowed to refer to a location outside/i,
|
||||
);
|
||||
|
||||
await expect(
|
||||
fs.exists(anotherMockDir.resolve('docs/index.md')),
|
||||
).resolves.toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('addBuildTimestampMetadata', () => {
|
||||
|
||||
@@ -396,8 +396,18 @@ export const patchIndexPreBuild = async ({
|
||||
path.join(inputDir, 'readme.md'),
|
||||
];
|
||||
|
||||
if (!isChildPath(inputDir, docsPath)) {
|
||||
throw new NotAllowedError(
|
||||
`Target path ${docsPath} is not allowed to refer to a location outside ${inputDir}`,
|
||||
);
|
||||
}
|
||||
await fs.ensureDir(docsPath);
|
||||
for (const filePath of fallbacks) {
|
||||
if (!isChildPath(inputDir, filePath)) {
|
||||
throw new NotAllowedError(
|
||||
`Source path ${filePath} is not allowed to refer to a location outside ${inputDir}`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
await fs.copyFile(filePath, indexMdPath);
|
||||
return;
|
||||
|
||||
@@ -122,11 +122,6 @@ export class TechdocsGenerator implements GeneratorBase {
|
||||
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
|
||||
const resolvedDocsDir = path.join(inputDir, docsDir ?? 'docs');
|
||||
await validateDocsDirectory(resolvedDocsDir, inputDir);
|
||||
|
||||
if (parsedLocationAnnotation) {
|
||||
await patchMkdocsYmlPreBuild(
|
||||
mkdocsYmlPath,
|
||||
@@ -140,6 +135,12 @@ export class TechdocsGenerator implements GeneratorBase {
|
||||
await patchIndexPreBuild({ inputDir, logger: childLogger, docsDir });
|
||||
}
|
||||
|
||||
// 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
|
||||
const resolvedDocsDir = path.join(inputDir, docsDir ?? 'docs');
|
||||
|
||||
await validateDocsDirectory(resolvedDocsDir, inputDir);
|
||||
|
||||
// patch the list of mkdocs plugins
|
||||
const defaultPlugins = this.options.defaultPlugins ?? [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user