Skip to content

Commit 2f5eb1e

Browse files
nullvariantclaude
andauthored
refactor: rename functions for clarity and consistency (#229)
* refactor: rename functions for clarity and consistency - Rename path safety functions (isPathSafe→isShellSafePath, etc.) - Rename SSH validation functions (isKeyLoaded→checkKeyLoadedInAgent, etc.) - Resolve function name shadowing (validateSshKeyPath→validateSshKeyPathFormat) - Improve check prefix functions (checkWorkspaceBoundary→assertWithinWorkspaceBoundary) - Rename validator objects (validateNoControlChars→controlCharValidator) - Improve helper function names (isSymbolicLink→isSymbolicLinkSafe) - Add naming convention documentation to validators/common.ts - Add deprecated aliases for backwards compatibility 🖥️ IDE: [Cursor](https://cursor.sh) 🔌 Extension: [Claude Code](https://claude.ai/download) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Model-Raw: claude-opus-4-5-20251101 * fix: remove incorrect @deprecated from isShellSafePath isShellSafePath checks for shell metacharacters and path traversal, while validatePathSecurity checks for filesystem attacks. They serve different purposes and cannot be substituted. Changed @deprecated to @see to indicate validatePathSecurity as an alternative for comprehensive security checks, not a replacement. 🖥️ IDE: [Cursor](https://cursor.sh) 🔌 Extension: [Claude Code](https://claude.ai/download) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Model-Raw: claude-opus-4-5-20251101 --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 36af79d commit 2f5eb1e

9 files changed

Lines changed: 253 additions & 84 deletions

File tree

extensions/git-id-switcher/src/identity/inputValidator.ts

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,18 @@ export interface ValidationResult {
3333
}
3434

3535
/**
36-
* Validate a string field for dangerous patterns
36+
* Validate a string field for dangerous command injection patterns.
37+
*
38+
* @remarks
39+
* **Naming convention**: Named with `validate` prefix and explicit purpose
40+
* (`ForDangerousPatterns`) because this function checks for security-critical
41+
* patterns like command injection, not format validity.
42+
*
43+
* @param value - The field value to validate
44+
* @param fieldName - The name of the field (for error messages)
45+
* @param errors - Array to accumulate validation errors
3746
*/
38-
function validateField(
47+
function validateFieldForDangerousPatterns(
3948
value: string | undefined,
4049
fieldName: string,
4150
errors: string[]
@@ -72,13 +81,23 @@ function validateEmail(email: string | undefined, errors: string[]): void {
7281
}
7382

7483
/**
75-
* Validate SSH key path
84+
* Validate SSH key path format.
7685
*
86+
* This validates only the format/structure of the path:
7787
* - Must be absolute or start with ~
7888
* - No path traversal (..)
7989
* - No dangerous characters
90+
*
91+
* @remarks
92+
* **Naming convention**: Named with `Format` suffix to distinguish from
93+
* `validateSshKeyPath` in pathUtils.ts which performs full path normalization
94+
* and symlink resolution. This function only validates format.
95+
*
96+
* **Terminology**:
97+
* - `valid`: Format/structure is correct (this function)
98+
* - `secure`: Resistant to security attacks (pathUtils.validateSshKeyPath)
8099
*/
81-
function validateSshKeyPath(
100+
function validateSshKeyPathFormat(
82101
sshKeyPath: string | undefined,
83102
errors: string[]
84103
): void {
@@ -97,7 +116,7 @@ function validateSshKeyPath(
97116
}
98117

99118
// No dangerous characters in path
100-
validateField(sshKeyPath, 'sshKeyPath', errors);
119+
validateFieldForDangerousPatterns(sshKeyPath, 'sshKeyPath', errors);
101120
}
102121

103122
/**
@@ -169,15 +188,15 @@ export function validateIdentity(identity: Identity): ValidationResult {
169188
}
170189

171190
// Text field validation (dangerous patterns)
172-
validateField(identity.name, 'name', errors);
173-
validateField(identity.email, 'email', errors);
174-
validateField(identity.service, 'service', errors);
175-
validateField(identity.description, 'description', errors);
176-
validateField(identity.icon, 'icon', errors);
191+
validateFieldForDangerousPatterns(identity.name, 'name', errors);
192+
validateFieldForDangerousPatterns(identity.email, 'email', errors);
193+
validateFieldForDangerousPatterns(identity.service, 'service', errors);
194+
validateFieldForDangerousPatterns(identity.description, 'description', errors);
195+
validateFieldForDangerousPatterns(identity.icon, 'icon', errors);
177196

178197
// Format-specific validation
179198
validateEmail(identity.email, errors);
180-
validateSshKeyPath(identity.sshKeyPath, errors);
199+
validateSshKeyPathFormat(identity.sshKeyPath, errors);
181200
validateGpgKeyId(identity.gpgKeyId, errors);
182201
validateSshHost(identity.sshHost, errors);
183202

@@ -235,18 +254,28 @@ export function validateIdentities(identities: Identity[]): ValidationResult {
235254
}
236255

237256
/**
238-
* Check if a path is safe (no injection risk)
257+
* Check if a path is safe for shell execution (no injection risk).
258+
*
259+
* Used for SSH key paths and other file system operations where the path
260+
* may be passed to shell commands.
239261
*
240-
* Used for SSH key paths and other file system operations.
262+
* @remarks
263+
* **Naming convention**: Named with `is` prefix (boolean predicate) and
264+
* `ShellSafe` to indicate this specifically checks for shell injection safety,
265+
* not comprehensive path security.
241266
*
242-
* @deprecated Use isSecurePath from security/pathValidator instead.
243-
* This function is kept for backwards compatibility and maintains the original
244-
* behavior of checking for path traversal and shell metacharacters.
267+
* **Terminology**:
268+
* - `safe`: Safe for a specific context (shell execution in this case)
269+
* - `secure`: Resistant to security attacks (use `validatePathSecurity` instead)
270+
*
271+
* @see validatePathSecurity from security/pathValidator for comprehensive
272+
* security checks. This function maintains the original behavior of checking
273+
* for path traversal and shell metacharacters only.
245274
*
246275
* @param path - The path to check
247-
* @returns true if the path appears safe
276+
* @returns true if the path appears safe for shell execution
248277
*/
249-
export function isPathSafe(path: string): boolean {
278+
export function isShellSafePath(path: string): boolean {
250279
// Original check: path traversal detection (simple check for "..")
251280
// This is stricter than isSecurePath's traversal detection
252281
if (hasPathTraversal(path)) {
@@ -262,3 +291,8 @@ export function isPathSafe(path: string): boolean {
262291

263292
return true;
264293
}
294+
295+
/**
296+
* @deprecated Use `isShellSafePath` instead. This alias will be removed in a future version.
297+
*/
298+
export const isPathSafe = isShellSafePath;

extensions/git-id-switcher/src/security/commandAllowlist.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
* @see https://owasp.org/www-project-application-security-verification-standard/
88
*/
99

10-
import { isSecurePath, isPathArgument } from './pathValidator';
10+
import { validatePathSecurity, isPathArgument } from './pathValidator';
1111
import { validateCombinedFlags } from './flagValidator';
1212

1313
// Re-export for backwards compatibility
14-
export { isSecurePath, isPathArgument, SecurePathResult } from './pathValidator';
14+
export { validatePathSecurity, isSecurePath, isPathArgument, SecurePathResult } from './pathValidator';
1515
export { validateCombinedFlags, CombinedFlagResult } from './flagValidator';
1616

1717
/**
@@ -172,10 +172,16 @@ function resolveSubcommandConfig(
172172
/* c8 ignore stop */
173173

174174
/**
175-
* Check if arg is a value for a previous option that expects values.
175+
* Check if an argument is a value for a preceding flag that expects values.
176+
*
177+
* @remarks
178+
* **Naming convention**: Named with `is` prefix (boolean-like predicate) and
179+
* explicit parameter description (`ArgumentValue`, `PrecedingFlag`) to clarify
180+
* the relationship between the current argument and the previous flag.
181+
*
176182
* @internal
177183
*/
178-
function isValueForPreviousOption(
184+
function isArgumentValueForPrecedingFlag(
179185
arg: string,
180186
index: number,
181187
argsToValidate: string[],
@@ -225,7 +231,7 @@ function validateFlagArgument(
225231
function validatePathSafety(arg: string): AllowlistCheckResult | 'safe' {
226232
const looksLikePathForSecurity = isPathArgument(arg) || arg.includes('/');
227233
if (looksLikePathForSecurity) {
228-
const pathResult = isSecurePath(arg);
234+
const pathResult = validatePathSecurity(arg);
229235
if (!pathResult.valid) {
230236
return { allowed: false, reason: `Path argument rejected: ${pathResult.reason}` };
231237
}
@@ -269,7 +275,7 @@ function validateSingleArgument(
269275
const looksLikePathForSecurity = isPathArgument(arg) || arg.includes('/');
270276

271277
// 1. Check if this is a value for a previous option
272-
const valueCheck = isValueForPreviousOption(arg, index, argsToValidate, allowedOptionsWithValues);
278+
const valueCheck = isArgumentValueForPrecedingFlag(arg, index, argsToValidate, allowedOptionsWithValues);
273279
if (valueCheck === 'is_value') {
274280
return 'continue';
275281
}

extensions/git-id-switcher/src/security/flagValidator.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,16 @@ const ALLOWED_COMBINED_PATTERNS: Record<string, readonly string[]> = {
3939
};
4040

4141
/**
42-
* Check for security-critical character issues in a flag.
42+
* Detect unsafe characters in a flag argument.
43+
*
44+
* @remarks
45+
* **Naming convention**: Named with `detect` prefix because this function
46+
* looks for and reports security issues (whitespace, null bytes, control chars,
47+
* invisible Unicode) rather than just validating format.
48+
*
4349
* @internal
4450
*/
45-
function checkFlagSecurityChars(flag: string): CombinedFlagResult | null {
51+
function detectUnsafeCharsInFlag(flag: string): CombinedFlagResult | null {
4652
// CRITICAL: Check for leading/trailing whitespace (potential obfuscation)
4753
if (flag !== flag.trim()) {
4854
return { valid: false, reason: 'Flag contains leading or trailing whitespace' };
@@ -98,10 +104,16 @@ function validateCombinedFlagChars(flagChars: string): CombinedFlagResult | null
98104
}
99105

100106
/**
101-
* Check combined flag against allowlist patterns and individual flags.
107+
* Validate combined flag characters against the allowlist.
108+
*
109+
* @remarks
110+
* **Naming convention**: Named with `validate` prefix because this returns
111+
* a result object with `valid` boolean and optional `reason`. The explicit
112+
* `AgainstAllowlist` suffix clarifies what it's being validated against.
113+
*
102114
* @internal
103115
*/
104-
function checkCombinedFlagAllowlist(
116+
function validateCombinedFlagAgainstAllowlist(
105117
flagChars: string,
106118
command: string,
107119
allowedArgs: readonly string[]
@@ -165,7 +177,7 @@ export function validateCombinedFlags(
165177
}
166178

167179
// CRITICAL: Security character checks (whitespace, null bytes, control chars, invisible unicode)
168-
const securityCheck = checkFlagSecurityChars(flag);
180+
const securityCheck = detectUnsafeCharsInFlag(flag);
169181
if (securityCheck) {
170182
return securityCheck;
171183
}
@@ -175,7 +187,7 @@ export function validateCombinedFlags(
175187

176188
/* c8 ignore start - Post-normalization check for edge cases in Unicode normalization */
177189
// CRITICAL: Re-check after normalization
178-
const normalizedSecurityCheck = checkFlagSecurityChars(normalizedFlag);
190+
const normalizedSecurityCheck = detectUnsafeCharsInFlag(normalizedFlag);
179191
if (normalizedSecurityCheck) {
180192
return {
181193
valid: false,
@@ -242,5 +254,5 @@ export function validateCombinedFlags(
242254
}
243255

244256
// Check against allowlist patterns and individual flags
245-
return checkCombinedFlagAllowlist(flagChars, command, allowedArgs);
257+
return validateCombinedFlagAgainstAllowlist(flagChars, command, allowedArgs);
246258
}

extensions/git-id-switcher/src/security/pathNormalizer.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import * as os from 'node:os';
99
import * as path from 'node:path';
1010
import * as fs from 'node:fs';
11-
import { isSecurePath, SecurePathResult } from './pathValidator';
11+
import { validatePathSecurity, SecurePathResult } from './pathValidator';
1212
import { PATH_MAX } from '../core/constants';
1313
import { hasNullByte, hasPathTraversal } from '../validators/common';
1414
import { resolveSymlinksSecurely } from './pathSymlinkResolver';
@@ -97,13 +97,18 @@ function validatePathLength(
9797
}
9898

9999
/**
100-
* Security check specifically for normalized paths
100+
* Security check specifically for normalized paths.
101101
*
102102
* After path.normalize(), we need to verify:
103103
* 1. Path doesn't escape to unexpected locations
104104
* 2. No remaining traversal patterns
105+
*
106+
* @remarks
107+
* **Naming convention**: Named with `validate` prefix and `Security` suffix because:
108+
* - `validate*()` returns a result object with `valid` boolean and optional `reason`
109+
* - `Security` indicates this checks for attack resistance post-normalization
105110
*/
106-
function isSecurePathAfterNormalization(
111+
function validateNormalizedPathSecurity(
107112
normalizedPath: string,
108113
originalPath: string
109114
): SecurePathResult {
@@ -166,7 +171,7 @@ function resolveAndValidateSymlinks(
166171
const resolvedPath = symlinkResult.resolvedPath ?? normalizedPath;
167172

168173
// Re-validate after symlink resolution
169-
const symlinkCheck = isSecurePathAfterNormalization(resolvedPath, inputPath);
174+
const symlinkCheck = validateNormalizedPathSecurity(resolvedPath, inputPath);
170175
if (!symlinkCheck.valid) {
171176
return { invalidResult: { valid: false, originalPath: inputPath, reason: `Post-symlink check failed: ${symlinkCheck.reason}` } };
172177
}
@@ -210,7 +215,7 @@ export function normalizeAndValidatePath(
210215
if (inputLengthCheck) return inputLengthCheck;
211216

212217
// Step 3: Pre-normalization security check on raw input
213-
const preCheck = isSecurePath(inputPath);
218+
const preCheck = validatePathSecurity(inputPath);
214219
if (!preCheck.valid) {
215220
return { valid: false, originalPath: inputPath, reason: `Pre-normalization check failed: ${preCheck.reason}` };
216221
}
@@ -233,7 +238,7 @@ export function normalizeAndValidatePath(
233238
/* c8 ignore stop */
234239

235240
// Step 6: Post-normalization security check
236-
const postCheck = isSecurePathAfterNormalization(normalizedPath, inputPath);
241+
const postCheck = validateNormalizedPathSecurity(normalizedPath, inputPath);
237242
/* c8 ignore start - Defense-in-depth: pre-check catches most issues, this is fallback */
238243
if (!postCheck.valid) {
239244
return { valid: false, originalPath: inputPath, reason: `Post-normalization check failed: ${postCheck.reason}` };

extensions/git-id-switcher/src/security/pathUnicodeDetector.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,33 @@ export const createInvisibleUnicodeValidator = (suffix: string = ''): Validator
5454
// Pre-built Validators
5555
// ============================================================================
5656

57-
/** Validates no control characters (pre-normalization) */
58-
export const validateNoControlChars = createControlCharValidator();
57+
/**
58+
* Validator that rejects paths containing control characters (pre-normalization).
59+
*
60+
* @remarks
61+
* **Naming convention**: Named as a validator object (noun) rather than a function
62+
* because it's a pre-built validator instance created by a factory function.
63+
*/
64+
export const controlCharValidator = createControlCharValidator();
65+
66+
/**
67+
* Validator that rejects paths containing invisible Unicode characters (pre-normalization).
68+
*
69+
* @remarks
70+
* **Naming convention**: Named as a validator object (noun) rather than a function
71+
* because it's a pre-built validator instance created by a factory function.
72+
*/
73+
export const invisibleUnicodeValidator = createInvisibleUnicodeValidator();
74+
75+
/**
76+
* @deprecated Use `controlCharValidator` instead. This alias will be removed in a future version.
77+
*/
78+
export const validateNoControlChars = controlCharValidator;
5979

60-
/** Validates no invisible Unicode characters (pre-normalization) */
61-
export const validateNoInvisibleUnicode = createInvisibleUnicodeValidator();
80+
/**
81+
* @deprecated Use `invisibleUnicodeValidator` instead. This alias will be removed in a future version.
82+
*/
83+
export const validateNoInvisibleUnicode = invisibleUnicodeValidator;
6284

6385
/**
6486
* Normalizes Unicode to NFC for consistent comparison

extensions/git-id-switcher/src/security/pathUtils.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,16 @@ function validateSubmodulePathPreChecks(
116116
}
117117

118118
/**
119-
* Check if normalized path is within workspace boundary.
119+
* Assert that normalized path is within workspace boundary.
120+
*
121+
* @remarks
122+
* **Naming convention**: Named with `assert` prefix because this function
123+
* returns an error result if the assertion fails (path escapes workspace),
124+
* or null if the assertion passes (path is within boundary).
125+
*
120126
* @internal
121127
*/
122-
function checkWorkspaceBoundary(
128+
function assertWithinWorkspaceBoundary(
123129
normalizedPath: string,
124130
workspacePath: string,
125131
originalPath: string
@@ -187,7 +193,7 @@ function verifySubmoduleSymlinks(
187193
const resolvedPath = resolvedByRealpath;
188194

189195
// SECURITY: Re-check workspace boundary after symlink resolution
190-
const boundaryCheck = checkWorkspaceBoundary(resolvedPath, normalizedWorkspace, originalPath);
196+
const boundaryCheck = assertWithinWorkspaceBoundary(resolvedPath, normalizedWorkspace, originalPath);
191197
if (boundaryCheck) {
192198
return {
193199
valid: false,
@@ -396,7 +402,7 @@ export function validateSubmodulePath(
396402
const normalizedSubmodulePath = result.normalizedPath!;
397403

398404
// SECURITY: Ensure the normalized path is still within workspace
399-
const boundaryCheck = checkWorkspaceBoundary(normalizedSubmodulePath, normalizedWorkspace, submodulePath);
405+
const boundaryCheck = assertWithinWorkspaceBoundary(normalizedSubmodulePath, normalizedWorkspace, submodulePath);
400406
/* c8 ignore start - Boundary escape after normalization (requires path traversal which is blocked earlier) */
401407
if (boundaryCheck) {
402408
return boundaryCheck;

0 commit comments

Comments
 (0)