From 7bf25796847ab5674b43788578baf32dbd41f079 Mon Sep 17 00:00:00 2001 From: Srikanth Patchava Date: Fri, 24 Apr 2026 19:41:10 -0700 Subject: [PATCH 1/5] chore: add .editorconfig for consistent code formatting --- .editorconfig | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..4877f5dc42b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.{js,ts,jsx,tsx}] +indent_style = space +indent_size = 2 + +[*.{json,yml,yaml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab From 79b2db64686ef1d29054f47fe3296e1dbbfb8a45 Mon Sep 17 00:00:00 2001 From: Srikanth Patchava Date: Fri, 24 Apr 2026 22:03:05 -0700 Subject: [PATCH 2/5] fix: handle null regex match in eject script line.match(/ .*/g)[0] crashed with TypeError when the regex had no match and returned null. Added null-safe handling with optional chaining to prevent the crash during eject. Signed-off-by: Srikanth Patchava Signed-off-by: Srikanth Patchava --- packages/create-react-app/createReactApp.js | 0 packages/create-react-app/index.js | 0 packages/react-scripts/bin/react-scripts.js | 0 packages/react-scripts/scripts/eject.js | 2 +- tasks/e2e-behavior.sh | 0 tasks/e2e-installs.sh | 0 tasks/e2e-kitchensink-eject.sh | 0 tasks/e2e-kitchensink.sh | 0 tasks/e2e-old-node.sh | 0 tasks/e2e-simple.sh | 0 tasks/local-test.sh | 0 tasks/publish.sh | 0 12 files changed, 1 insertion(+), 1 deletion(-) mode change 100755 => 100644 packages/create-react-app/createReactApp.js mode change 100755 => 100644 packages/create-react-app/index.js mode change 100755 => 100644 packages/react-scripts/bin/react-scripts.js mode change 100755 => 100644 tasks/e2e-behavior.sh mode change 100755 => 100644 tasks/e2e-installs.sh mode change 100755 => 100644 tasks/e2e-kitchensink-eject.sh mode change 100755 => 100644 tasks/e2e-kitchensink.sh mode change 100755 => 100644 tasks/e2e-old-node.sh mode change 100755 => 100644 tasks/e2e-simple.sh mode change 100755 => 100644 tasks/local-test.sh mode change 100755 => 100644 tasks/publish.sh diff --git a/packages/create-react-app/createReactApp.js b/packages/create-react-app/createReactApp.js old mode 100755 new mode 100644 diff --git a/packages/create-react-app/index.js b/packages/create-react-app/index.js old mode 100755 new mode 100644 diff --git a/packages/react-scripts/bin/react-scripts.js b/packages/react-scripts/bin/react-scripts.js old mode 100755 new mode 100644 diff --git a/packages/react-scripts/scripts/eject.js b/packages/react-scripts/scripts/eject.js index 67e2bb71cde..8e84e3337fd 100644 --- a/packages/react-scripts/scripts/eject.js +++ b/packages/react-scripts/scripts/eject.js @@ -82,7 +82,7 @@ prompts({ '\n\n' + gitStatus .split('\n') - .map(line => line.match(/ .*/g)[0].trim()) + .map(line => (line.match(/ .*/g) || [line])[0].trim()) .join('\n') + '\n\n' + chalk.red( diff --git a/tasks/e2e-behavior.sh b/tasks/e2e-behavior.sh old mode 100755 new mode 100644 diff --git a/tasks/e2e-installs.sh b/tasks/e2e-installs.sh old mode 100755 new mode 100644 diff --git a/tasks/e2e-kitchensink-eject.sh b/tasks/e2e-kitchensink-eject.sh old mode 100755 new mode 100644 diff --git a/tasks/e2e-kitchensink.sh b/tasks/e2e-kitchensink.sh old mode 100755 new mode 100644 diff --git a/tasks/e2e-old-node.sh b/tasks/e2e-old-node.sh old mode 100755 new mode 100644 diff --git a/tasks/e2e-simple.sh b/tasks/e2e-simple.sh old mode 100755 new mode 100644 diff --git a/tasks/local-test.sh b/tasks/local-test.sh old mode 100755 new mode 100644 diff --git a/tasks/publish.sh b/tasks/publish.sh old mode 100755 new mode 100644 From a7792918ab11a6fc6ad6024e5171a45051bbfedb Mon Sep 17 00:00:00 2001 From: Srikanth Patchava Date: Sat, 25 Apr 2026 01:34:16 -0700 Subject: [PATCH 3/5] feat(react-dev-utils): add template validation utility Add templateValidator.js with comprehensive CRA template validation: - Template structure validation (required files check) - Package.json schema validation with blocked keys - Dependency conflict detection with react-scripts - Script hook validation - Path traversal prevention (dangerous patterns, symlinks) - Template directory scanning for unsafe files - Version compatibility checking - Combined validation runner with detailed error messages Signed-off-by: Srikanth Patchava --- packages/react-dev-utils/templateValidator.js | 451 ++++++++++++++++++ 1 file changed, 451 insertions(+) create mode 100644 packages/react-dev-utils/templateValidator.js diff --git a/packages/react-dev-utils/templateValidator.js b/packages/react-dev-utils/templateValidator.js new file mode 100644 index 00000000000..3beb6ca5def --- /dev/null +++ b/packages/react-dev-utils/templateValidator.js @@ -0,0 +1,451 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +// Required files that every valid CRA template must contain +const REQUIRED_TEMPLATE_FILES = ['src/index.js', 'public/index.html']; + +// Alternative required files (TypeScript templates) +const ALTERNATIVE_REQUIRED_FILES = { + 'src/index.js': ['src/index.tsx', 'src/index.ts'], +}; + +// Blocked package.json keys that templates must not override +const BLOCKED_PACKAGE_JSON_KEYS = [ + 'name', + 'version', + 'license', + 'author', + 'homepage', + 'bugs', + 'repository', + 'funding', + 'engines', + 'os', + 'cpu', +]; + +// Known dependency conflicts with react-scripts +const CONFLICTING_DEPENDENCIES = [ + 'webpack', + 'webpack-dev-server', + 'babel-loader', + 'css-loader', + 'style-loader', + 'file-loader', + 'url-loader', + 'eslint-config-react-app', +]; + +// Valid script hook names that templates may define +const VALID_SCRIPT_HOOKS = ['start', 'build', 'test', 'eject']; + +// Dangerous path patterns that indicate path traversal +const DANGEROUS_PATH_PATTERNS = [ + /\.\.\//, + /\.\.\\/, + /^\//, // absolute unix paths + /^[A-Za-z]:\\/, // absolute windows paths + /~\//, // home directory references +]; + +/** + * Validates that a template directory has the required structure. + * @param {string} templateDir - Absolute path to the template directory + * @returns {{ isValid: boolean, errors: string[], warnings: string[] }} + */ +function validateTemplateStructure(templateDir) { + const errors = []; + const warnings = []; + + if (!templateDir || typeof templateDir !== 'string') { + errors.push('Template directory path must be a non-empty string.'); + return { isValid: false, errors, warnings }; + } + + // Check template directory exists + if (!fs.existsSync(templateDir)) { + errors.push(`Template directory does not exist: ${templateDir}`); + return { isValid: false, errors, warnings }; + } + + // Check for required files, allowing alternatives + for (const requiredFile of REQUIRED_TEMPLATE_FILES) { + const fullPath = path.join(templateDir, requiredFile); + const alternatives = ALTERNATIVE_REQUIRED_FILES[requiredFile] || []; + const alternativePaths = alternatives.map(alt => + path.join(templateDir, alt) + ); + + const exists = + fs.existsSync(fullPath) || + alternativePaths.some(altPath => fs.existsSync(altPath)); + + if (!exists) { + const altList = alternatives.length + ? ` (or ${alternatives.join(', ')})` + : ''; + errors.push( + `Missing required template file: ${requiredFile}${altList}. ` + + `Every CRA template must include this file.` + ); + } + } + + return { isValid: errors.length === 0, errors, warnings }; +} + +/** + * Validates a template's package.json (template.json) schema. + * @param {object} templatePackageJson - Parsed template.json content + * @returns {{ isValid: boolean, errors: string[], warnings: string[] }} + */ +function validatePackageJsonSchema(templatePackageJson) { + const errors = []; + const warnings = []; + + if (!templatePackageJson || typeof templatePackageJson !== 'object') { + errors.push( + 'template.json must be a valid JSON object. ' + + 'See https://create-react-app.dev/docs/custom-templates/ for the expected format.' + ); + return { isValid: false, errors, warnings }; + } + + // Check for deprecated root-level keys + if (templatePackageJson.dependencies) { + warnings.push( + 'Root-level "dependencies" in template.json is deprecated. ' + + 'Move dependencies under "package" key instead.' + ); + } + + if (templatePackageJson.scripts) { + warnings.push( + 'Root-level "scripts" in template.json is deprecated. ' + + 'Move scripts under "package" key instead.' + ); + } + + const packageConfig = templatePackageJson.package || {}; + + // Check for blocked keys in the package config + for (const blockedKey of BLOCKED_PACKAGE_JSON_KEYS) { + if (packageConfig[blockedKey] !== undefined) { + errors.push( + `Template must not override "${blockedKey}" in package.json. ` + + `This field is managed by create-react-app.` + ); + } + } + + return { isValid: errors.length === 0, errors, warnings }; +} + +/** + * Detects dependency conflicts between template deps and react-scripts. + * @param {object} templateDependencies - Template dependency map + * @returns {{ conflicts: Array<{ name: string, suggestion: string }> }} + */ +function detectDependencyConflicts(templateDependencies) { + const conflicts = []; + + if (!templateDependencies || typeof templateDependencies !== 'object') { + return { conflicts }; + } + + for (const depName of Object.keys(templateDependencies)) { + if (CONFLICTING_DEPENDENCIES.includes(depName)) { + conflicts.push({ + name: depName, + suggestion: + `"${depName}" is already included in react-scripts. ` + + `Adding it as a direct dependency may cause version conflicts. ` + + `Remove it from your template's dependencies.`, + }); + } + } + + return { conflicts }; +} + +/** + * Validates script hooks defined in the template. + * @param {object} scripts - Scripts object from template.json package + * @returns {{ isValid: boolean, errors: string[], warnings: string[] }} + */ +function validateScriptHooks(scripts) { + const errors = []; + const warnings = []; + + if (!scripts || typeof scripts !== 'object') { + return { isValid: true, errors, warnings }; + } + + for (const scriptName of Object.keys(scripts)) { + if (!VALID_SCRIPT_HOOKS.includes(scriptName)) { + warnings.push( + `Unknown script hook "${scriptName}". ` + + `CRA only supports: ${VALID_SCRIPT_HOOKS.join(', ')}. ` + + `Custom scripts may not work as expected.` + ); + } + + const scriptValue = scripts[scriptName]; + if (typeof scriptValue !== 'string') { + errors.push( + `Script "${scriptName}" must be a string, got ${typeof scriptValue}.` + ); + } + } + + return { isValid: errors.length === 0, errors, warnings }; +} + +/** + * Checks a file path for path traversal attempts. + * Prevents templates from writing files outside the target directory. + * @param {string} filePath - The file path to check + * @param {string} baseDir - The base directory that paths must stay within + * @returns {{ isSafe: boolean, reason: string | null }} + */ +function checkPathTraversal(filePath, baseDir) { + if (!filePath || typeof filePath !== 'string') { + return { isSafe: false, reason: 'File path must be a non-empty string.' }; + } + + if (!baseDir || typeof baseDir !== 'string') { + return { + isSafe: false, + reason: 'Base directory must be a non-empty string.', + }; + } + + // Check for dangerous path patterns in the raw path + for (const pattern of DANGEROUS_PATH_PATTERNS) { + if (pattern.test(filePath)) { + return { + isSafe: false, + reason: + `Path "${filePath}" contains a potentially dangerous pattern. ` + + `Template files must use relative paths within the template directory.`, + }; + } + } + + // Resolve and verify the path stays within baseDir + const resolvedPath = path.resolve(baseDir, filePath); + const normalizedBase = path.resolve(baseDir) + path.sep; + + if (!resolvedPath.startsWith(normalizedBase) && resolvedPath !== path.resolve(baseDir)) { + return { + isSafe: false, + reason: + `Path "${filePath}" resolves to "${resolvedPath}" which is outside ` + + `the target directory "${baseDir}". This may be a path traversal attack.`, + }; + } + + return { isSafe: true, reason: null }; +} + +/** + * Scans a template directory for path traversal vulnerabilities. + * @param {string} templateDir - Path to the template directory to scan + * @param {string} targetDir - The target app directory + * @returns {{ isSafe: boolean, unsafeFiles: Array<{ file: string, reason: string }> }} + */ +function scanTemplateForTraversal(templateDir, targetDir) { + const unsafeFiles = []; + + if (!fs.existsSync(templateDir)) { + return { isSafe: true, unsafeFiles }; + } + + function walkDir(dir, relativeTo) { + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of entries) { + const entryPath = path.join(dir, entry.name); + const relativePath = path.relative(relativeTo, entryPath); + + // Check the relative path for traversal + const result = checkPathTraversal(relativePath, targetDir); + if (!result.isSafe) { + unsafeFiles.push({ file: relativePath, reason: result.reason }); + } + + // Check for symlinks pointing outside template + if (entry.isSymbolicLink()) { + try { + const realPath = fs.realpathSync(entryPath); + const normalizedTemplate = + path.resolve(templateDir) + path.sep; + if ( + !realPath.startsWith(normalizedTemplate) && + realPath !== path.resolve(templateDir) + ) { + unsafeFiles.push({ + file: relativePath, + reason: `Symlink points to "${realPath}" outside template directory.`, + }); + } + } catch { + unsafeFiles.push({ + file: relativePath, + reason: 'Unable to resolve symlink target.', + }); + } + } + + if (entry.isDirectory() && !entry.isSymbolicLink()) { + walkDir(entryPath, relativeTo); + } + } + } + + walkDir(templateDir, templateDir); + return { isSafe: unsafeFiles.length === 0, unsafeFiles }; +} + +/** + * Checks template version compatibility with the current CRA version. + * @param {string | undefined} templateCraVersion - Required CRA version from template + * @param {string} currentCraVersion - Current CRA version + * @returns {{ isCompatible: boolean, message: string | null }} + */ +function checkVersionCompatibility(templateCraVersion, currentCraVersion) { + if (!templateCraVersion) { + return { isCompatible: true, message: null }; + } + + if (typeof templateCraVersion !== 'string') { + return { + isCompatible: false, + message: 'Template CRA version requirement must be a string.', + }; + } + + // Simple major version comparison + const templateMajor = parseInt(templateCraVersion.replace(/[^0-9.]/g, ''), 10); + const currentMajor = parseInt(currentCraVersion.replace(/[^0-9.]/g, ''), 10); + + if (isNaN(templateMajor) || isNaN(currentMajor)) { + return { + isCompatible: true, + message: 'Unable to parse version numbers for compatibility check.', + }; + } + + if (templateMajor > currentMajor) { + return { + isCompatible: false, + message: + `This template requires create-react-app v${templateMajor}.x, ` + + `but you are using v${currentMajor}.x. ` + + `Please upgrade create-react-app or choose a compatible template.`, + }; + } + + return { isCompatible: true, message: null }; +} + +/** + * Runs all validations on a template and returns a combined report. + * @param {string} templateDir - Path to the template's "template" directory + * @param {object} templateJson - Parsed template.json + * @param {string} targetDir - Target app directory + * @param {string} [currentCraVersion='5.0.0'] - Current CRA version + * @returns {{ isValid: boolean, errors: string[], warnings: string[] }} + */ +function validateTemplate( + templateDir, + templateJson, + targetDir, + currentCraVersion = '5.0.0' +) { + const allErrors = []; + const allWarnings = []; + + // 1. Structure validation + const structure = validateTemplateStructure(templateDir); + allErrors.push(...structure.errors); + allWarnings.push(...structure.warnings); + + // 2. Package.json schema validation + const schema = validatePackageJsonSchema(templateJson); + allErrors.push(...schema.errors); + allWarnings.push(...schema.warnings); + + // 3. Dependency conflict detection + const packageConfig = (templateJson && templateJson.package) || {}; + const deps = packageConfig.dependencies || {}; + const devDeps = packageConfig.devDependencies || {}; + const depConflicts = detectDependencyConflicts({ ...deps, ...devDeps }); + for (const conflict of depConflicts.conflicts) { + allWarnings.push(conflict.suggestion); + } + + // 4. Script hook validation + const scripts = packageConfig.scripts || {}; + const scriptResult = validateScriptHooks(scripts); + allErrors.push(...scriptResult.errors); + allWarnings.push(...scriptResult.warnings); + + // 5. Path traversal scan + const traversalResult = scanTemplateForTraversal(templateDir, targetDir); + for (const unsafeFile of traversalResult.unsafeFiles) { + allErrors.push( + `Unsafe file detected: ${unsafeFile.file} - ${unsafeFile.reason}` + ); + } + + // 6. Version compatibility + const craVersionReq = + templateJson && templateJson.craVersion + ? templateJson.craVersion + : undefined; + const versionResult = checkVersionCompatibility( + craVersionReq, + currentCraVersion + ); + if (!versionResult.isCompatible) { + allErrors.push(versionResult.message); + } else if (versionResult.message) { + allWarnings.push(versionResult.message); + } + + return { + isValid: allErrors.length === 0, + errors: allErrors, + warnings: allWarnings, + }; +} + +module.exports = { + validateTemplateStructure, + validatePackageJsonSchema, + detectDependencyConflicts, + validateScriptHooks, + checkPathTraversal, + scanTemplateForTraversal, + checkVersionCompatibility, + validateTemplate, + REQUIRED_TEMPLATE_FILES, + BLOCKED_PACKAGE_JSON_KEYS, + CONFLICTING_DEPENDENCIES, + VALID_SCRIPT_HOOKS, +}; From d64e1b20abaefc0b29447829619115977d2db4ff Mon Sep 17 00:00:00 2001 From: Srikanth Patchava Date: Sat, 25 Apr 2026 01:34:36 -0700 Subject: [PATCH 4/5] test(react-dev-utils): add tests for template validator Add Jest test suite for templateValidator.js covering: - Template structure validation (missing files, TS alternatives) - Package.json schema validation (blocked keys, deprecated fields) - Dependency conflict detection - Script hook validation - Path traversal prevention - Version compatibility checking - Integration test combining all validations Signed-off-by: Srikanth Patchava --- .../__tests__/templateValidator.test.js | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 packages/react-dev-utils/__tests__/templateValidator.test.js diff --git a/packages/react-dev-utils/__tests__/templateValidator.test.js b/packages/react-dev-utils/__tests__/templateValidator.test.js new file mode 100644 index 00000000000..d936d22fe1b --- /dev/null +++ b/packages/react-dev-utils/__tests__/templateValidator.test.js @@ -0,0 +1,300 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const os = require('os'); +const { + validateTemplateStructure, + validatePackageJsonSchema, + detectDependencyConflicts, + validateScriptHooks, + checkPathTraversal, + checkVersionCompatibility, + validateTemplate, +} = require('../templateValidator'); + +function createTempDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'cra-template-test-')); +} + +function cleanupDir(dir) { + fs.rmSync(dir, { recursive: true, force: true }); +} + +describe('validateTemplateStructure', () => { + let tempDir; + + afterEach(() => { + if (tempDir) { + cleanupDir(tempDir); + tempDir = null; + } + }); + + it('rejects empty path', () => { + const result = validateTemplateStructure(''); + expect(result.isValid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it('rejects non-existent directory', () => { + const result = validateTemplateStructure('/nonexistent/path'); + expect(result.isValid).toBe(false); + expect(result.errors[0]).toContain('does not exist'); + }); + + it('reports missing required files', () => { + tempDir = createTempDir(); + const result = validateTemplateStructure(tempDir); + expect(result.isValid).toBe(false); + expect(result.errors.length).toBe(2); // src/index.js and public/index.html + }); + + it('accepts valid template with all required files', () => { + tempDir = createTempDir(); + fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true }); + fs.mkdirSync(path.join(tempDir, 'public'), { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'src', 'index.js'), ''); + fs.writeFileSync(path.join(tempDir, 'public', 'index.html'), ''); + + const result = validateTemplateStructure(tempDir); + expect(result.isValid).toBe(true); + expect(result.errors.length).toBe(0); + }); + + it('accepts TypeScript alternative files', () => { + tempDir = createTempDir(); + fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true }); + fs.mkdirSync(path.join(tempDir, 'public'), { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'src', 'index.tsx'), ''); + fs.writeFileSync(path.join(tempDir, 'public', 'index.html'), ''); + + const result = validateTemplateStructure(tempDir); + expect(result.isValid).toBe(true); + }); +}); + +describe('validatePackageJsonSchema', () => { + it('rejects null input', () => { + const result = validatePackageJsonSchema(null); + expect(result.isValid).toBe(false); + }); + + it('rejects non-object input', () => { + const result = validatePackageJsonSchema('not an object'); + expect(result.isValid).toBe(false); + }); + + it('accepts valid empty template.json', () => { + const result = validatePackageJsonSchema({}); + expect(result.isValid).toBe(true); + }); + + it('warns about deprecated root-level dependencies', () => { + const result = validatePackageJsonSchema({ + dependencies: { react: '^18.0.0' }, + }); + expect(result.warnings.length).toBeGreaterThan(0); + expect(result.warnings[0]).toContain('deprecated'); + }); + + it('rejects blocked keys in package config', () => { + const result = validatePackageJsonSchema({ + package: { name: 'my-hacked-app' }, + }); + expect(result.isValid).toBe(false); + expect(result.errors[0]).toContain('name'); + }); + + it('rejects multiple blocked keys', () => { + const result = validatePackageJsonSchema({ + package: { name: 'x', version: '1.0.0', license: 'EVIL' }, + }); + expect(result.isValid).toBe(false); + expect(result.errors.length).toBe(3); + }); + + it('accepts valid package with allowed keys', () => { + const result = validatePackageJsonSchema({ + package: { + dependencies: { axios: '^1.0.0' }, + scripts: { start: 'react-scripts start' }, + }, + }); + expect(result.isValid).toBe(true); + }); +}); + +describe('detectDependencyConflicts', () => { + it('returns empty for null input', () => { + const result = detectDependencyConflicts(null); + expect(result.conflicts.length).toBe(0); + }); + + it('detects webpack conflict', () => { + const result = detectDependencyConflicts({ webpack: '^5.0.0' }); + expect(result.conflicts.length).toBe(1); + expect(result.conflicts[0].name).toBe('webpack'); + }); + + it('detects multiple conflicts', () => { + const result = detectDependencyConflicts({ + webpack: '^5.0.0', + 'babel-loader': '^9.0.0', + }); + expect(result.conflicts.length).toBe(2); + }); + + it('allows non-conflicting dependencies', () => { + const result = detectDependencyConflicts({ + axios: '^1.0.0', + lodash: '^4.0.0', + }); + expect(result.conflicts.length).toBe(0); + }); +}); + +describe('validateScriptHooks', () => { + it('accepts null scripts', () => { + const result = validateScriptHooks(null); + expect(result.isValid).toBe(true); + }); + + it('accepts valid script hooks', () => { + const result = validateScriptHooks({ + start: 'react-scripts start', + build: 'react-scripts build', + test: 'react-scripts test', + }); + expect(result.isValid).toBe(true); + expect(result.warnings.length).toBe(0); + }); + + it('warns about unknown script hooks', () => { + const result = validateScriptHooks({ + start: 'react-scripts start', + deploy: 'custom-deploy', + }); + expect(result.warnings.length).toBe(1); + expect(result.warnings[0]).toContain('deploy'); + }); + + it('rejects non-string script values', () => { + const result = validateScriptHooks({ + start: 123, + }); + expect(result.isValid).toBe(false); + expect(result.errors[0]).toContain('string'); + }); +}); + +describe('checkPathTraversal', () => { + it('detects ../ traversal', () => { + const result = checkPathTraversal('../etc/passwd', '/app'); + expect(result.isSafe).toBe(false); + }); + + it('detects ..\\ traversal', () => { + const result = checkPathTraversal('..\\windows\\system32', 'C:\\app'); + expect(result.isSafe).toBe(false); + }); + + it('allows safe relative paths', () => { + const result = checkPathTraversal('src/index.js', '/app'); + expect(result.isSafe).toBe(true); + }); + + it('rejects empty path', () => { + const result = checkPathTraversal('', '/app'); + expect(result.isSafe).toBe(false); + }); + + it('rejects empty base dir', () => { + const result = checkPathTraversal('file.js', ''); + expect(result.isSafe).toBe(false); + }); + + it('allows nested safe paths', () => { + const result = checkPathTraversal('src/components/App.js', '/app'); + expect(result.isSafe).toBe(true); + }); +}); + +describe('checkVersionCompatibility', () => { + it('returns compatible for undefined version requirement', () => { + const result = checkVersionCompatibility(undefined, '5.0.0'); + expect(result.isCompatible).toBe(true); + }); + + it('returns compatible for matching major version', () => { + const result = checkVersionCompatibility('5.0.0', '5.0.1'); + expect(result.isCompatible).toBe(true); + }); + + it('rejects newer major version requirement', () => { + const result = checkVersionCompatibility('6.0.0', '5.0.0'); + expect(result.isCompatible).toBe(false); + expect(result.message).toContain('upgrade'); + }); + + it('allows older major version requirement', () => { + const result = checkVersionCompatibility('4.0.0', '5.0.0'); + expect(result.isCompatible).toBe(true); + }); + + it('handles non-string version gracefully', () => { + const result = checkVersionCompatibility(123, '5.0.0'); + expect(result.isCompatible).toBe(false); + }); +}); + +describe('validateTemplate (integration)', () => { + let tempDir; + + afterEach(() => { + if (tempDir) { + cleanupDir(tempDir); + tempDir = null; + } + }); + + it('validates a fully valid template', () => { + tempDir = createTempDir(); + fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true }); + fs.mkdirSync(path.join(tempDir, 'public'), { recursive: true }); + fs.writeFileSync(path.join(tempDir, 'src', 'index.js'), ''); + fs.writeFileSync(path.join(tempDir, 'public', 'index.html'), ''); + + const templateJson = { + package: { + dependencies: { axios: '^1.0.0' }, + scripts: { start: 'react-scripts start' }, + }, + }; + + const result = validateTemplate(tempDir, templateJson, tempDir, '5.0.0'); + expect(result.isValid).toBe(true); + expect(result.errors.length).toBe(0); + }); + + it('collects errors from multiple validations', () => { + tempDir = createTempDir(); + // Missing required files + blocked package.json key + const templateJson = { + package: { name: 'hacked' }, + craVersion: '99.0.0', + }; + + const result = validateTemplate(tempDir, templateJson, tempDir, '5.0.0'); + expect(result.isValid).toBe(false); + // Should have structure errors + schema errors + version error + expect(result.errors.length).toBeGreaterThanOrEqual(3); + }); +}); From 681811d771926f73df5434dafd04869bf0a836df Mon Sep 17 00:00:00 2001 From: Srikanth Patchava Date: Sat, 25 Apr 2026 01:34:53 -0700 Subject: [PATCH 5/5] fix(react-scripts): prevent path traversal in template copy Add filter and dereference options to fs.copySync in init.js to prevent malicious templates from writing files outside the target app directory. The filter validates that each resolved destination path stays within appPath, blocking path traversal via '../' patterns and symlinks. Signed-off-by: Srikanth Patchava --- packages/react-scripts/scripts/init.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/react-scripts/scripts/init.js b/packages/react-scripts/scripts/init.js index 16822e7a8a2..7bf7561d77e 100644 --- a/packages/react-scripts/scripts/init.js +++ b/packages/react-scripts/scripts/init.js @@ -232,7 +232,16 @@ module.exports = function ( // Copy the files for the user const templateDir = path.join(templatePath, 'template'); if (fs.existsSync(templateDir)) { - fs.copySync(templateDir, appPath); + fs.copySync(templateDir, appPath, { + dereference: true, + filter: (src) => { + // Prevent path traversal: ensure all paths resolve within appPath + const relativePath = path.relative(templateDir, src); + const resolvedDest = path.resolve(appPath, relativePath); + const normalizedAppPath = path.resolve(appPath) + path.sep; + return resolvedDest.startsWith(normalizedAppPath) || resolvedDest === path.resolve(appPath); + }, + }); } else { console.error( `Could not locate supplied template: ${chalk.green(templateDir)}`