Skip to content

Commit b103f3b

Browse files
committed
refactor(@angular/cli): standardize update command git utility execution
All `git` commands now use `execFileSync` instead of `execSync` to prevent shell injection vulnerabilities and provide more predictable execution. `checkCleanGit` now utilizes `git status --porcelain -z` for NUL-terminated output, ensuring correct handling of filenames with spaces or special characters, and preventing potential path trimming bugs. An `execGit` helper function was introduced to reduce code duplication and standardize `git` command execution options. `hasChangesToCommit` now gracefully handles non-Git repositories by returning `false` instead of throwing.
1 parent f7a4354 commit b103f3b

File tree

1 file changed

+59
-26
lines changed
  • packages/angular/cli/src/commands/update/utilities

1 file changed

+59
-26
lines changed

‎packages/angular/cli/src/commands/update/utilities/git.ts‎

Lines changed: 59 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,57 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import{execSync}from'node:child_process';
9+
import{execFileSync}from'node:child_process';
1010
import*aspathfrom'node:path';
1111

12+
/**
13+
* Execute a git command.
14+
* @param args Arguments to pass to the git command.
15+
* @param input Optional input to pass to the command via stdin.
16+
* @returns The output of the command.
17+
*/
18+
functionexecGit(args: string[],input?: string): string{
19+
returnexecFileSync('git',args,{encoding: 'utf8',stdio: 'pipe', input });
20+
}
21+
1222
/**
1323
* Checks if the git repository is clean.
14-
* @param root The root directory of the project.
15-
* @returns True if the repository is clean, false otherwise.
24+
* This function only checks for changes that are within the specified root directory.
25+
* Changes outside the root directory are ignored.
26+
* @param root The root directory of the project to check.
27+
* @returns True if the repository is clean within the root, false otherwise.
1628
*/
1729
exportfunctioncheckCleanGit(root: string): boolean{
1830
try{
19-
consttopLevel=execSync('git rev-parse --show-toplevel',{
20-
encoding: 'utf8',
21-
stdio: 'pipe',
22-
});
23-
constresult=execSync('git status --porcelain',{encoding: 'utf8',stdio: 'pipe'});
24-
if(result.trim().length===0){
31+
consttopLevel=execGit(['rev-parse','--show-toplevel']);
32+
constresult=execGit(['status','--porcelain','-z']);
33+
if(result.length===0){
2534
returntrue;
2635
}
2736

28-
// Only files inside the workspace root are relevant
29-
for(constentryofresult.split('\n')){
30-
constrelativeEntry=path.relative(
31-
path.resolve(root),
32-
path.resolve(topLevel.trim(),entry.slice(3).trim()),
33-
);
37+
constentries=result.split('\0');
38+
for(leti=0;i<entries.length;i++){
39+
constline=entries[i];
40+
if(!line){
41+
continue;
42+
}
43+
44+
// Status is the first 2 characters.
45+
// If the status is a rename ('R'), the next entry in the split array is the target path.
46+
letfilePath=line.slice(3);
47+
conststatus=line.slice(0,2);
48+
if(status[0]==='R'){
49+
// Check the source path (filePath)
50+
if(isPathInsideRoot(filePath,root,topLevel.trim())){
51+
returnfalse;
52+
}
53+
54+
// The next entry is the target path of the rename.
55+
i++;
56+
filePath=entries[i];
57+
}
3458

35-
if(!relativeEntry.startsWith('..')&&!path.isAbsolute(relativeEntry)){
59+
if(isPathInsideRoot(filePath,root,topLevel.trim())){
3660
returnfalse;
3761
}
3862
}
@@ -41,15 +65,24 @@ export function checkCleanGit(root: string): boolean{
4165
returntrue;
4266
}
4367

68+
functionisPathInsideRoot(filePath: string,root: string,topLevel: string): boolean{
69+
constrelativeEntry=path.relative(path.resolve(root),path.resolve(topLevel,filePath));
70+
71+
return!relativeEntry.startsWith('..')&&!path.isAbsolute(relativeEntry);
72+
}
73+
4474
/**
4575
* Checks if the working directory has pending changes to commit.
46-
* @returns Whether or not the working directory has Git changes to commit.
76+
* @returns Whether or not the working directory has Git changes to commit. Returns false if not in a Git repository.
4777
*/
4878
exportfunctionhasChangesToCommit(): boolean{
49-
// List all modified files not covered by .gitignore.
50-
// If any files are returned, then there must be something to commit.
51-
52-
returnexecSync('git ls-files -m -d -o --exclude-standard').toString()!=='';
79+
try{
80+
// List all modified files not covered by .gitignore.
81+
// If any files are returned, then there must be something to commit.
82+
returnexecGit(['ls-files','-m','-d','-o','--exclude-standard']).trim()!=='';
83+
}catch{
84+
returnfalse;
85+
}
5386
}
5487

5588
/**
@@ -58,19 +91,19 @@ export function hasChangesToCommit(): boolean{
5891
*/
5992
exportfunctioncreateCommit(message: string){
6093
// Stage entire working tree for commit.
61-
execSync('git add -A',{encoding: 'utf8',stdio: 'pipe'});
94+
execGit(['add','-A']);
6295

6396
// Commit with the message passed via stdin to avoid bash escaping issues.
64-
execSync('git commit--no-verify -F -',{encoding: 'utf8',stdio: 'pipe',input: message});
97+
execGit(['commit','--no-verify','-F','-'],message);
6598
}
6699

67100
/**
68-
* Finds the Git SHA hash of the HEAD commit.
69-
* @returns The Git SHA hash of the HEAD commit. Returns null if unable to retrieve the hash.
101+
* Finds the full Git SHA hash of the HEAD commit.
102+
* @returns The full Git SHA hash of the HEAD commit. Returns null if unable to retrieve the hash.
70103
*/
71104
exportfunctionfindCurrentGitSha(): string|null{
72105
try{
73-
returnexecSync('git rev-parse HEAD',{encoding: 'utf8',stdio: 'pipe'}).trim();
106+
returnexecGit(['rev-parse','HEAD']).trim();
74107
}catch{
75108
returnnull;
76109
}

0 commit comments

Comments
(0)