Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/php-wasm/node/src/test/write-files.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ describe('writeFiles', () => {
]);
});

it('rejects file paths outside the target directory', async () => {
await expect(
writeFiles(php, '/test', {
'../escape.txt': 'file',
})
).rejects.toThrow('File paths must stay inside the target directory.');

expect(php.fileExists('/escape.txt')).toBe(false);
});

it('removes any pre-existing when ran with rmRoot: true', async () => {
php.writeFile('/test/file.txt', 'file');
php.mkdir('/test/subdirectory1');
Expand Down
13 changes: 10 additions & 3 deletions packages/php-wasm/universal/src/lib/write-files.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { dirname, joinPaths } from '@php-wasm/util';
import { dirname, isParentOf, joinPaths } from '@php-wasm/util';
import type { UniversalPHP } from './universal-php';

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface FileTree
extends Record<string, Uint8Array | string | FileTree> {}
export interface FileTree extends Record<
string,
Uint8Array | string | FileTree
> {}

export interface WriteFilesOptions {
/**
Expand Down Expand Up @@ -43,6 +45,11 @@ export async function writeFiles(
}
for (const [relativePath, content] of Object.entries(newFiles)) {
const filePath = joinPaths(root, relativePath);
if (!isParentOf(root, filePath)) {
throw new Error(
'File paths must stay inside the target directory.'
);
}
Comment on lines +48 to +52
if (!(await php.fileExists(dirname(filePath)))) {
await php.mkdir(dirname(filePath));
}
Expand Down
10 changes: 9 additions & 1 deletion packages/playground/blueprints/src/lib/steps/install-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Directory } from '../v1/resources';
import { importThemeStarterContent } from './import-theme-starter-content';
import { zipNameToHumanName } from '../utils/zip-name-to-human-name';
import { writeFiles } from '@php-wasm/universal';
import { joinPaths } from '@php-wasm/util';
import { basename, joinPaths } from '@php-wasm/util';
import { logger } from '@php-wasm/logger';

/**
Expand Down Expand Up @@ -122,6 +122,14 @@ export const installTheme: StepHandler<
} else {
assetNiceName = themeData.name;
assetFolderName = targetFolderName || assetNiceName;
if (
!assetFolderName ||
basename(assetFolderName) !== assetFolderName
) {
throw new Error(
'Theme folder name must be a single directory name.'
);
}
Comment on lines 124 to +132

progress?.tracker.setCaption(
`Installing the ${progressName()} theme`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,67 @@ describe('Blueprint step installTheme', () => {
expect(php.fileExists(expectedThemeIndexPhpPath)).toBe(true);
});

it('should reject directory theme names outside the themes directory', async () => {
await expect(
installTheme(php, {
themeData: {
name: '../escape',
files: {
'index.php': `/**\n * Theme Name: Test Theme`,
},
},
options: {
activate: false,
},
})
).rejects.toThrow(
'Theme folder name must be a single directory name.'
);

expect(php.fileExists('/wordpress/wp-content/escape')).toBe(false);
});

it('should reject directory theme targetFolderName values with subdirectories', async () => {
await expect(
installTheme(php, {
themeData: {
name: 'test-theme',
files: {
'index.php': `/**\n * Theme Name: Test Theme`,
},
},
options: {
activate: false,
targetFolderName: 'nested/theme',
},
})
).rejects.toThrow(
'Theme folder name must be a single directory name.'
);

expect(php.fileExists('/wordpress/wp-content/themes/nested')).toBe(
false
);
});

it('should reject directory theme file paths outside the theme directory', async () => {
await expect(
installTheme(php, {
themeData: {
name: 'test-theme',
files: {
'../escape.php': `/**\n * Theme Name: Test Theme`,
},
},
options: {
activate: false,
},
})
).rejects.toThrow('File paths must stay inside the target directory.');

expect(php.fileExists('/wordpress/wp-content/escape.php')).toBe(false);
});

it('should skip installation errors when onError is skip-theme', async () => {
const loggerWarnSpy = vi
.spyOn(logger, 'warn')
Expand Down
Loading