-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add security fixes, quality tools, and infrastructure improvements #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add security fixes, quality tools, and infrastructure improvements #13
Conversation
Security fixes: - Fix prototype pollution timing in state-manager.ts (sanitize BEFORE validation) - Add temp file cleanup to prevent orphaned files accumulation - Add ReDoS protection to file-searcher.ts globToRegex function - Add file size limits to feature-analyzer.ts New tools: - stackshift_spec_quality: Analyze spec quality with scores for completeness, testability, and clarity - stackshift_spec_diff: Compare specifications between directories New infrastructure: - error-handler.ts: Standardized error wrapping and safe execution utilities - logger.ts: Structured logging with levels, JSON/pretty output modes - spec-alignment.yml: GitHub Actions workflow for CI/CD spec validation New slash commands: - /stackshift.quality: Analyze specification quality - /stackshift.diff: Compare specifications between directories Test updates: - Updated state-manager test to verify sanitize-first behavior All 446 tests pass.
Performance optimizations: - Pre-compile regex patterns at module level in gap-analyzer.ts - Move common words to Set for O(1) lookup - Use structured logger instead of console.log Error handling improvements: - Integrate error-handler.ts into analyze.ts tool - Add logging to previously silent catch blocks - Use consistent error wrapping pattern New utilities: - validation.ts: Centralized input validation with common validators - maxLength, minLength, range, pattern, oneOf - safeString, safeGlobPattern, safePath - validateOrThrow, createObjectValidator - Sanitization helpers (sanitizeString, escapeHtml) All 446 tests pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds comprehensive security improvements, quality analysis tools, and infrastructure enhancements to the StackShift MCP server. It addresses prototype pollution vulnerabilities, adds ReDoS protection, implements structured logging and error handling, and introduces two new specification quality tools with GitHub Actions integration.
Key Changes:
- Security fixes for prototype pollution, temp file cleanup, ReDoS protection, and file size limits
- New quality analysis tools (spec-quality and spec-diff) with scoring algorithms for completeness, testability, and clarity
- Infrastructure modules for validation, error handling, and structured logging
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| mcp-server/src/utils/validation.ts | New validation module with string length, pattern, and safety validators |
| mcp-server/src/utils/state-manager.ts | Security fix to sanitize JSON before validation and add temp file cleanup |
| mcp-server/src/utils/logger.ts | New structured logging module with level-based filtering and environment-aware output |
| mcp-server/src/utils/error-handler.ts | New error handling utilities with standardized wrapping and safe execution |
| mcp-server/src/utils/tests/state-manager.test.ts | Updated test to verify sanitize-first behavior |
| mcp-server/src/tools/spec-quality.ts | New tool to analyze spec quality with scoring on completeness, testability, clarity |
| mcp-server/src/tools/spec-diff.ts | New tool to compare specifications between directories |
| mcp-server/src/tools/analyze.ts | Integration of new error handler for consistent error management |
| mcp-server/src/index.ts | Registration of new tools and slash commands |
| mcp-server/src/analyzers/utils/file-searcher.ts | Added ReDoS protection to glob pattern processing |
| mcp-server/src/analyzers/gap-analyzer.ts | Performance improvements with pre-compiled regex patterns |
| mcp-server/src/analyzers/feature-analyzer.ts | Added file size limits and improved error handling |
| .github/workflows/spec-alignment.yml | New GitHub Actions workflow for CI/CD spec validation |
| .claude/commands/stackshift.quality.md | Documentation for quality analysis slash command |
| .claude/commands/stackshift.diff.md | Documentation for spec diff slash command |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| // This path is only taken when rethrow is explicitly false | ||
| throw wrappedError; |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function always throws regardless of the rethrow option. Line 105 is unreachable and contradicts the comment. If rethrow is false, the function should return a default value or the error should be handled differently. Either remove lines 104-105 or change the logic to return instead of throw when rethrow is false.
| throw wrappedError; | |
| return undefined as unknown as T; |
| // Reset regex lastIndex for global pattern (required for reuse) | ||
| DEPENDENCY_PATTERN.lastIndex = 0; | ||
|
|
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resetting lastIndex on a global regex is correct practice, but the regex is defined with the 'gi' flags on line 41, making it stateful. However, the code uses .match() on line 735 which doesn't use lastIndex. The .match() method with a global regex returns all matches at once. Either remove the lastIndex reset (since .match() doesn't use it) or use .exec() in a loop if you need stateful matching.
| // Reset regex lastIndex for global pattern (required for reuse) | |
| DEPENDENCY_PATTERN.lastIndex = 0; |
| * Create a safe string validator (no dangerous characters) | ||
| */ | ||
| export function safeString(): Validator<string> { | ||
| const dangerousChars = /[<>&'";\x00-\x1f]/; |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The safeString validator includes semicolon as a dangerous character, but sanitizeString (line 285) doesn't remove semicolons. This inconsistency means a string could pass validation but not be properly sanitized, or vice versa. Consider aligning the character sets between validation and sanitization, or document why they differ.
| } | ||
|
|
||
| // Check for passive voice (simple heuristic) | ||
| const passiveMatches = content.match(/\b(is|are|was|were|be|been|being)\s+\w+ed\b/gi) || []; |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This regex could cause ReDoS with pathological input containing many 'is/are/was/were' followed by non-word characters. The \w+ed pattern can backtrack excessively. Consider using a non-backtracking approach or limiting the input size more strictly (currently 2MB). A safer pattern would be: /\b(is|are|was|were|be|been|being)\s+\w+?ed\b/gi with non-greedy quantifier.
| const passiveMatches = content.match(/\b(is|are|was|were|be|been|being)\s+\w+ed\b/gi) || []; | |
| const passiveMatches = content.match(/\b(is|are|was|were|be|been|being)\s+\w+?ed\b/gi) || []; |
| .replace(/\*/g, '[^/]*?') // * matches anything except / (non-greedy) | ||
| .replace(/§DOUBLESTAR§/g, '.*?') // ** matches anything including / (non-greedy) |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While adding non-greedy quantifiers (? instead of ) is good for ReDoS protection, the patterns [^/]? and .? still have potential for backtracking in certain edge cases (e.g., 'a*b' pattern against 'aaaaaac'). Consider documenting this limitation or using possessive quantifiers if the regex engine supports them, or validate that the non-greedy approach combined with the length/wildcard limits on lines 265-274 is sufficient protection.
|
|
||
| // Check for incomplete markers | ||
| for (const marker of INCOMPLETE_MARKERS) { | ||
| const count = (content.match(new RegExp(escapeRegex(marker), 'gi')) || []).length; |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new RegExp inside a loop (for each marker) is inefficient. Consider pre-compiling the regexes for incomplete markers at module level, similar to how TESTABLE_PATTERNS and UNTESTABLE_PATTERNS are defined. This would improve performance when analyzing multiple specs.
| .filter(w => w.length > 3 && !commonWords.includes(w)); | ||
| .replace(NON_ALPHANUM_PATTERN, ' ') | ||
| .split(WHITESPACE_PATTERN) | ||
| .filter(w => w.length > 3 && !COMMON_WORDS.has(w)); |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The magic number '3' for minimum word length is not documented. Consider extracting this to a named constant (e.g., MIN_KEYWORD_LENGTH = 3) at the module level to make it clear why 3-letter words are filtered out and to make it easier to adjust in the future.
| * Create a safe glob pattern validator | ||
| */ | ||
| export function safeGlobPattern(): Validator<string> { | ||
| return (value: string) => { | ||
| // Check for pathological patterns | ||
| const consecutiveWildcards = (value.match(/\*[^/*]*\*/g) || []).length; | ||
| if (consecutiveWildcards > 3) { | ||
| return 'pattern has too many consecutive wildcards (potential ReDoS)'; | ||
| } | ||
| if (value.length > 500) { | ||
| return 'pattern is too long (max 500 characters)'; | ||
| } | ||
| return true; | ||
| }; |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The safeGlobPattern validator uses different logic (line 103 regex and > 3 threshold) than the file-searcher's ReDoS protection (line 271 in file-searcher.ts uses the same regex but same threshold). While they use the same threshold, having duplicate ReDoS detection logic in two places creates a maintenance burden. Consider extracting this validation to a shared utility function to ensure consistency.
| * Create a safe glob pattern validator | |
| */ | |
| export function safeGlobPattern(): Validator<string> { | |
| return (value: string) => { | |
| // Check for pathological patterns | |
| const consecutiveWildcards = (value.match(/\*[^/*]*\*/g) || []).length; | |
| if (consecutiveWildcards > 3) { | |
| return 'pattern has too many consecutive wildcards (potential ReDoS)'; | |
| } | |
| if (value.length > 500) { | |
| return 'pattern is too long (max 500 characters)'; | |
| } | |
| return true; | |
| }; | |
| * Shared logic for glob pattern ReDoS protection | |
| */ | |
| export function isSafeGlobPattern(value: string): true | string { | |
| // Check for pathological patterns | |
| const consecutiveWildcards = (value.match(/\*[^/*]*\*/g) || []).length; | |
| if (consecutiveWildcards > 3) { | |
| return 'pattern has too many consecutive wildcards (potential ReDoS)'; | |
| } | |
| if (value.length > 500) { | |
| return 'pattern is too long (max 500 characters)'; | |
| } | |
| return true; | |
| } | |
| /** | |
| * Create a safe glob pattern validator | |
| */ | |
| export function safeGlobPattern(): Validator<string> { | |
| return (value: string) => isSafeGlobPattern(value); |
| import { createConstitutionToolHandler } from './tools/create-constitution.js'; | ||
| import { createFeatureSpecsToolHandler } from './tools/create-feature-specs.js'; | ||
| import { createImplPlansToolHandler } from './tools/create-impl-plans.js'; | ||
| import { specQualityTool, executeSpecQuality } from './tools/spec-quality.js'; |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import specQualityTool.
| import { specQualityTool, executeSpecQuality } from './tools/spec-quality.js'; | |
| import { executeSpecQuality } from './tools/spec-quality.js'; |
| import { createFeatureSpecsToolHandler } from './tools/create-feature-specs.js'; | ||
| import { createImplPlansToolHandler } from './tools/create-impl-plans.js'; | ||
| import { specQualityTool, executeSpecQuality } from './tools/spec-quality.js'; | ||
| import { specDiffTool, executeSpecDiff } from './tools/spec-diff.js'; |
Copilot
AI
Nov 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import specDiffTool.
| import { specDiffTool, executeSpecDiff } from './tools/spec-diff.js'; | |
| import { executeSpecDiff } from './tools/spec-diff.js'; |
Security fixes:
New tools:
testability, and clarity
New infrastructure:
New slash commands:
Test updates:
All 446 tests pass.