Skip to content

Commit 2b75f17

Browse files
nullvariantclaude
andauthored
docs(architecture): update for new src/ directory structure + fix ReDoS (#223)
* docs(architecture): update for new src/ directory structure Update ARCHITECTURE.md to reflect the refactored directory structure: - Update all file path references to new locations - Replace logical grouping with actual directory structure - Expand security layer documentation with all 13 files - Update Command Execution Flow diagram - Fix table alignments for consistency 🖥️ 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(security): replace EMAIL_REGEX with split-based validation Replace regex-based email validation with split-based approach to prevent potential ReDoS (Regular Expression Denial of Service): - Deprecate EMAIL_REGEX constant (kept for backward compatibility) - Implement isValidEmail() using string operations instead of regex - Add length limit (254 chars per RFC 5321) - Add ReDoS resistance tests SonarQube: typescript:S5852 🖥️ 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 * chore: update version to 0.14.3 and fix table styles - Bump version to 0.14.3 - Add changelog entry for ReDoS fix and ARCHITECTURE.md update - Fix markdown table alignment per prettier-markdown 🖥️ 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 3f45b8c commit 2b75f17

7 files changed

Lines changed: 200 additions & 129 deletions

File tree

extensions/git-id-switcher/CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.14.3] - 2026-01-25
11+
12+
### Security
13+
14+
- **ReDoS Vulnerability Fix**: Replaced regex-based email validation with split-based approach
15+
- `EMAIL_REGEX` marked as deprecated (kept for backward compatibility)
16+
- New `isValidEmail()` function uses string operations instead of regex
17+
- Added length limit (254 chars per RFC 5321)
18+
- SonarQube: typescript:S5852
19+
20+
### Changed
21+
22+
- **ARCHITECTURE.md Updated**: Synchronized documentation with new directory structure
23+
- Updated 30+ file path references to reflect Phase 1-5 refactoring
24+
- Replaced logical grouping diagram with actual directory structure
25+
- Expanded security layer documentation with all 13 files
26+
1027
## [0.14.2] - 2026-01-23
1128

1229
### Fixed

extensions/git-id-switcher/docs/ARCHITECTURE.md

Lines changed: 123 additions & 121 deletions
Large diffs are not rendered by default.

extensions/git-id-switcher/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "git-id-switcher",
33
"displayName": "%extension.displayName%",
44
"description": "%extension.description%",
5-
"version": "0.14.2",
5+
"version": "0.14.3",
66
"publisher": "nullvariant",
77
"icon": "images/icon.png",
88
"engines": {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*/
1010

1111
import { Identity } from './identity';
12-
import { EMAIL_REGEX, hasPathTraversal } from '../validators/common';
12+
import { isValidEmail, hasPathTraversal } from '../validators/common';
1313

1414
export interface ValidationResult {
1515
valid: boolean;
@@ -70,11 +70,12 @@ function validateEmail(email: string | undefined, errors: string[]): void {
7070
return;
7171
}
7272

73-
if (!EMAIL_REGEX.test(email)) {
73+
if (!isValidEmail(email)) {
7474
errors.push('email: invalid email format');
7575
}
7676

7777
// Additional length check (RFC 5321)
78+
// Note: isValidEmail already checks for 254 chars, but identity config allows 320
7879
if (email.length > 320) {
7980
errors.push('email: exceeds maximum length (320 characters)');
8081
}

extensions/git-id-switcher/src/test/validatorsCommon.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,19 @@ function testIsValidEmail(): void {
162162
assert.strictEqual(isValidEmail('user @example.com'), false, 'Email with space should fail');
163163
assert.strictEqual(isValidEmail('user<tag>@example.com'), false, 'Email with angle brackets should fail');
164164

165+
// Additional edge cases (ReDoS prevention)
166+
assert.strictEqual(isValidEmail(''), false, 'Empty string should fail');
167+
assert.strictEqual(isValidEmail('user@@example.com'), false, 'Multiple @ should fail');
168+
assert.strictEqual(isValidEmail('user@example.com.'), false, 'Domain ending with dot should fail');
169+
assert.strictEqual(isValidEmail('a'.repeat(255) + '@example.com'), false, 'Email exceeding 254 chars should fail');
170+
171+
// ReDoS resistance test - should complete quickly even with adversarial input
172+
const start = Date.now();
173+
const adversarialInput = 'a'.repeat(100) + '@' + 'b'.repeat(100) + '.' + 'c'.repeat(50);
174+
isValidEmail(adversarialInput);
175+
const elapsed = Date.now() - start;
176+
assert.ok(elapsed < 100, `ReDoS test should complete in <100ms, took ${elapsed}ms`);
177+
165178
console.log('✅ isValidEmail tests passed!');
166179
}
167180

extensions/git-id-switcher/src/ui/documentationInternal.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export const DOCUMENT_HASHES: Record<string, string> = {
2525
'AGENTS.md': '54f16b767e57686b3eb46a2b4aa02b378554cc492c32c49ed96588f6d184b6b8',
2626
'CODE_OF_CONDUCT.md': 'a5eb286c902437bbe0f6d409894f20e51c172fa869fe2f151bfa388f9d911b54',
2727
'CONTRIBUTING.md': '4150f8810aec7b2e8eff9f3c69ee1bae1374843f50a812efa6778cba27a833cd',
28-
'extensions/git-id-switcher/CHANGELOG.md': '864fa5cd55227ade6c688cfcfe373473c9c564d89d17332ad46ceadc396986a9',
29-
'extensions/git-id-switcher/docs/ARCHITECTURE.md': '47797f0b92d987c3102968befc974661d3af28664e442538cc9ac6e3df9a18f4',
28+
'extensions/git-id-switcher/CHANGELOG.md': 'be7dae0e7edf02b42bf886dae0c54ac19f26f69f65b58ea0c4344b72155e5f16',
29+
'extensions/git-id-switcher/docs/ARCHITECTURE.md': 'a7f9efa1eefdc535d516aa46357cffc916c8cb982e6b8943152cd06818d7ec1c',
3030
'extensions/git-id-switcher/docs/CONTRIBUTING.md': '7d6ad2bc4d8c838790754cb9df848cb65f9fdce7e1a13e5c965b83a3d5b6378c',
3131
'extensions/git-id-switcher/docs/DESIGN_PHILOSOPHY.md': 'f9718b61ac161cb466dbc76845688e7acacf4e5fdc4b8b9553269dba4a094f6b',
3232
'extensions/git-id-switcher/docs/i18n/ain/README.md': '4d309165e22a943b2a1791914db02bc2762d5fb643f5350a2426558ac7b3ae19',

extensions/git-id-switcher/src/validators/common.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ export const CONTROL_CHAR_REGEX_ALL = /[\x00-\x1f\x7f]/;
4747
/**
4848
* Email validation regex (simplified RFC 5322)
4949
*
50-
* Allows most valid email addresses while rejecting obvious invalid ones.
51-
* Used in validation.ts and configSchema.ts.
50+
* @deprecated Use isValidEmail() instead. This regex is kept for backward
51+
* compatibility but should not be used directly due to potential ReDoS concerns.
52+
* @see isValidEmail
5253
*/
5354
export const EMAIL_REGEX = /^[^\s@<>]+@[^\s@<>]+\.[^\s@<>]+$/;
5455

@@ -160,11 +161,48 @@ export function hasInvisibleUnicode(s: string): boolean {
160161
/**
161162
* Validate an email address format
162163
*
164+
* Uses a split-based approach to avoid ReDoS vulnerabilities.
165+
* Validates: non-empty local part, single @, domain with at least one dot.
166+
*
163167
* @param email - The email string to validate
164168
* @returns true if email format is valid
165169
*/
166170
export function isValidEmail(email: string): boolean {
167-
return EMAIL_REGEX.test(email);
171+
// Length limit (RFC 5321: 254 chars max for email address)
172+
if (email.length > 254 || email.length === 0) {
173+
return false;
174+
}
175+
176+
// No whitespace allowed
177+
if (/\s/.test(email)) {
178+
return false;
179+
}
180+
181+
// No angle brackets allowed
182+
if (email.includes('<') || email.includes('>')) {
183+
return false;
184+
}
185+
186+
// Split by @ - must have exactly one @
187+
const atIndex = email.indexOf('@');
188+
if (atIndex === -1 || atIndex !== email.lastIndexOf('@')) {
189+
return false;
190+
}
191+
192+
const local = email.slice(0, atIndex);
193+
const domain = email.slice(atIndex + 1);
194+
195+
// Local part and domain must be non-empty
196+
if (local.length === 0 || domain.length === 0) {
197+
return false;
198+
}
199+
200+
// Domain must contain at least one dot and not end with dot
201+
if (!domain.includes('.') || domain.endsWith('.')) {
202+
return false;
203+
}
204+
205+
return true;
168206
}
169207

170208
/**

0 commit comments

Comments
 (0)