Fix ReDoS in PowerShell prompt detection#279853
Merged
meganrogge merged 2 commits intomicrosoft:mainfrom Dec 11, 2025
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a ReDoS (Regular Expression Denial of Service) vulnerability in the PowerShell prompt detection regex within the detectsInputRequiredPattern function. The vulnerability could allow an attacker to cause exponential backtracking by providing specially crafted input strings with many consecutive spaces.
Key Changes
- Modified the regex pattern in
outputMonitor.tsto eliminate ambiguity in whitespace matching - Changed
[^\[]+to(?:[^\[\s]|\s+(?!\[))+to ensure deterministic consumption of whitespace - The fix prevents the regex engine from exploring exponential combinations when backtracking
Contributor
Author
|
Hello @Tyriar, please find the results of the fix : node reproduce_issue_embedded.js
Total Log Content Length: 15909 characters
Chars | Old (ms) | Proposed (ms) | Match Equal
----------------------------------------------------------------
500 | 1.7053 | 0.0683 | YES
600 | 2.9498 | 0.0822 | YES
700 | 10.7116 | 0.1032 | YES
800 | 17.9709 | 0.0973 | YES
1100 | 28.3592 | 0.0710 | YES
1200 | 85.5718 | 0.0702 | YES
1300 | 153.2329 | 0.0690 | YES
1400 | 471.8805 | 0.0858 | YES
1500 | 938.9027 | 0.0793 | YES
1600 | 1341.8973 | 0.1117 | YES
1650 | 2855.1964 | 0.0872 | YES
1700 | 4281.7549 | 0.0855 | YES
1800 | TIMEOUT | 0.0932 | NO
1900 | TIMEOUT | 0.0963 | NO
2000 | TIMEOUT | 0.0977 | NO
3000 | TIMEOUT | 0.1550 | NO
4000 | TIMEOUT | 0.2316 | NO
5000 | TIMEOUT | 0.3792 | NO
10000 | TIMEOUT | 1.4454 | NO
15000 | TIMEOUT | 3.3913 | NO
15909 | TIMEOUT | 3.7709 | NOAnd the code to reproduce the issue bellow : const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
// Console log output involved in the issue
const logContent = `info: TransformationPipeline.Services.PipelineInitializer[0]
Initializing all pipelines...
info: TransformationPipeline.Services.TransformationService[0]
Loading pipeline configuration (default: /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config, override: /app/config-override)
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Loading default configuration from /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Loading pipeline configuration from /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config/pipelines.yaml
info: TransformationPipeline.Services.TracePipeline[0]
Loading trace pipeline configuration (default: /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config/traces, override: /app/config-override/traces)
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Loading pipeline configuration from /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config/traces/pipelines.yaml
info: TransformationPipeline.Services.MetricPipeline[0]
Loading metric pipeline configuration (default: /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config/metrics, override: /app/config-override/metrics)
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Loading pipeline configuration from /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config/metrics/pipelines.yaml
info: TransformationPipeline.Services.ProfilePipeline[0]
Loading profile pipeline configuration (default: /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config/profiles, override: /app/config-override/profiles)
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Loading pipeline configuration from /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config/profiles/pipelines.yaml
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Loaded 1 pipeline(s) from /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config/metrics/pipelines.yaml
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Resolving imports for 1 pipeline(s)
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Loaded 11 pipeline(s) from /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config/profiles/pipelines.yaml
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Resolving imports for 11 pipeline(s)
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Loaded 14 pipeline(s) from /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config/pipelines.yaml
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Resolving imports for 14 pipeline(s)
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Loaded 8 pipeline(s) from /Users/rducom/sources/poc-pipeline/transformation-pipeline/tools/PipelineTester/bin/Debug/net10.0/config/traces/pipelines.yaml
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
Resolving imports for 8 pipeline(s)
info: TransformationPipeline.Services.MetricPipeline[0]
Successfully loaded 1 metric pipeline(s)
info: TransformationPipeline.Services.MetricPipeline[0]
Metric pipeline 'metrics-passthrough' initialized with 0 processor(s), filter: '*'
info: TransformationPipeline.Services.MetricPipeline[0]
MetricPipeline initialized with 1 active pipeline(s)
warn: TransformationPipeline.Configuration.PipelineImportResolver[0]
Import file sources/http-server-spans.yaml contains no pipelines
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'http-server-traces' resolved with 0 processor(s) (0 imported + 0 own)
warn: TransformationPipeline.Configuration.PipelineImportResolver[0]
Import file sources/http-client-spans.yaml contains no pipelines
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'http-client-traces' resolved with 0 processor(s) (0 imported + 0 own)
warn: TransformationPipeline.Configuration.PipelineImportResolver[0]
Import file sources/database-spans.yaml contains no pipelines
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'database-traces' resolved with 0 processor(s) (0 imported + 0 own)
warn: TransformationPipeline.Configuration.PipelineImportResolver[0]
Import file sources/cpu_profiles.yaml contains no pipelines
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'cpu-profiles' resolved with 0 processor(s) (0 imported + 0 own)
warn: TransformationPipeline.Configuration.PipelineImportResolver[0]
Import file sources/kubernetes-spans.yaml contains no pipelines
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'kubernetes-traces' resolved with 0 processor(s) (0 imported + 0 own)
info: TransformationPipeline.Services.TracePipeline[0]
Successfully loaded 8 trace pipeline(s)
warn: TransformationPipeline.Configuration.PipelineImportResolver[0]
Import file sources/memory_profiles.yaml contains no pipelines
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'memory-profiles' resolved with 0 processor(s) (0 imported + 0 own)
info: TransformationPipeline.Services.TracePipeline[0]
Trace pipeline 'global-trace-normalizer' initialized with 1 processor(s), filter: '*'
warn: TransformationPipeline.Configuration.PipelineImportResolver[0]
Import file sources/goroutine_profiles.yaml contains no pipelines
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'goroutine-profiles' resolved with 0 processor(s) (0 imported + 0 own)
info: TransformationPipeline.Services.TracePipeline[0]
Trace pipeline 'filter-health-checks' initialized with 1 processor(s), filter: 'span.name:/health|/ping|/ready|/live'
info: TransformationPipeline.Services.TracePipeline[0]
Trace pipeline 'http-server-traces' initialized with 0 processor(s), filter: 'span.kind:Server'
info: TransformationPipeline.Services.TracePipeline[0]
Trace pipeline 'http-client-traces' initialized with 0 processor(s), filter: 'span.kind:Client'
info: TransformationPipeline.Services.TracePipeline[0]
Trace pipeline 'database-traces' initialized with 0 processor(s), filter: 'span_attributes.db.system:*'
info: TransformationPipeline.Services.TracePipeline[0]
Trace pipeline 'kubernetes-traces' initialized with 0 processor(s), filter: 'resource_attributes.k8s.pod.name:*'
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'kubernetes-audit-base' resolved with 6 processor(s) (1 imported + 5 own)
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'kubernetes-audit' resolved with 6 processor(s) (6 imported + 0 own)
warn: TransformationPipeline.Configuration.PipelineImportResolver[0]
Import file sources/block_profiles.yaml contains no pipelines
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'block-profiles' resolved with 0 processor(s) (0 imported + 0 own)
warn: TransformationPipeline.Configuration.PipelineImportResolver[0]
Import file sources/mutex_profiles.yaml contains no pipelines
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'mutex-profiles' resolved with 0 processor(s) (0 imported + 0 own)
info: TransformationPipeline.Services.TracePipeline[0]
Trace pipeline 'error-traces' initialized with 2 processor(s), filter: 'span.status_code:Error'
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'kubernetes-events-base' resolved with 3 processor(s) (1 imported + 2 own)
info: TransformationPipeline.Services.TracePipeline[0]
TracePipeline initialized with 7 pipeline(s)
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'kubernetes-events' resolved with 3 processor(s) (3 imported + 0 own)
warn: TransformationPipeline.Configuration.PipelineImportResolver[0]
Import file sources/kubernetes_profiles.yaml contains no pipelines
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'kubernetes-profiles' resolved with 0 processor(s) (0 imported + 0 own)
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'talos-os' resolved with 1 processor(s) (1 imported + 0 own)
warn: TransformationPipeline.Configuration.PipelineImportResolver[0]
Import file sources/application_profiles.yaml contains no pipelines
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'application-profiles' resolved with 0 processor(s) (0 imported + 0 own)
info: TransformationPipeline.Services.ProfilePipeline[0]
Successfully loaded 11 profile pipeline(s)
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'otel-collector-internal' resolved with 1 processor(s) (1 imported + 0 own)
info: TransformationPipeline.Services.ProfilePipeline[0]
Profile pipeline 'filter-otel-internal-profiles' initialized with 1 processor(s), filter: 'profile.type == "cpu" and resource.service.name =~ "otel-collector|opentelemetry"'
info: TransformationPipeline.Services.ProfilePipeline[0]
Profile pipeline 'filter-profiler-overhead' initialized with 1 processor(s), filter: 'profile.type == "cpu" and (function_name =~ "pprof|profiler|agent" or file_name =~ "profiling|pprof")'
info: TransformationPipeline.Services.ProfilePipeline[0]
Profile pipeline 'cpu-profiles' initialized with 0 processor(s), filter: 'profile.type == "cpu"'
info: TransformationPipeline.Services.ProfilePipeline[0]
Profile pipeline 'memory-profiles' initialized with 0 processor(s), filter: 'profile.type == "memory" or sample.type =~ "alloc|inuse"'
info: TransformationPipeline.Services.ProfilePipeline[0]
Profile pipeline 'goroutine-profiles' initialized with 0 processor(s), filter: 'profile.type == "goroutine" or sample.type == "goroutines"'
info: TransformationPipeline.Services.ProfilePipeline[0]
Profile pipeline 'block-profiles' initialized with 0 processor(s), filter: 'profile.type == "block" or sample.type =~ "contentions|delay"'
info: TransformationPipeline.Services.ProfilePipeline[0]
Profile pipeline 'mutex-profiles' initialized with 0 processor(s), filter: 'profile.type == "mutex" or sample.type =~ "mutex|lock"'
info: TransformationPipeline.Services.ProfilePipeline[0]
Profile pipeline 'kubernetes-profiles' initialized with 0 processor(s), filter: 'resource.k8s.namespace.name != null'
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'argocd' resolved with 1 processor(s) (1 imported + 0 own)
info: TransformationPipeline.Services.ProfilePipeline[0]
Profile pipeline 'high-sample-count' initialized with 2 processor(s), filter: 'sample_count > 10000'
info: TransformationPipeline.Services.ProfilePipeline[0]
Profile pipeline 'deep-stacktraces' initialized with 2 processor(s), filter: 'max_stack_depth > 100'
info: TransformationPipeline.Services.ProfilePipeline[0]
Profile pipeline 'application-profiles' initialized with 0 processor(s), filter: 'signal.type == "profile"'
info: TransformationPipeline.Services.ProfilePipeline[0]
ProfilePipeline initialized with 11 active pipeline(s)
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'strimzi-operator' resolved with 3 processor(s) (3 imported + 0 own)
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'kafka-broker' resolved with 2 processor(s) (2 imported + 0 own)
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'cilium' resolved with 1 processor(s) (1 imported + 0 own)
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'otel-http' resolved with 9 processor(s) (9 imported + 0 own)
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'otel-messaging' resolved with 8 processor(s) (8 imported + 0 own)
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'otel-exceptions' resolved with 5 processor(s) (5 imported + 0 own)
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'otel-generic' resolved with 3 processor(s) (3 imported + 0 own)
info: TransformationPipeline.Configuration.PipelineImportResolver[0]
Pipeline 'unknown-fallback' resolved with 1 processor(s) (1 imported + 0 own)
info: TransformationPipeline.Configuration.ConfigurationLoader[0]
No override configuration found. Using default configuration only.
info: TransformationPipeline.Services.TransformationService[0]
Successfully loaded 14 pipeline(s)
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'global-casing-normalizer' initialized with 1 processor(s), filter: '*'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'kubernetes-audit' initialized with 6 processor(s), filter: 'apiVersion:audit.k8s.io*'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'kubernetes-events' initialized with 3 processor(s), filter: 'event.domain:*'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'talos-os' initialized with 1 processor(s), filter: 'talos.service:*'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'otel-collector-internal' initialized with 1 processor(s), filter: 'otelcol.component.id:*'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'argocd' initialized with 1 processor(s), filter: 'resource.container.image.name:*argoproj/argocd*'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'strimzi-operator' initialized with 3 processor(s), filter: 'resource.container.image.name:*strimzi/operator*'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'kafka-broker' initialized with 2 processor(s), filter: 'resource.container.image.name:*strimzi/kafka*'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'cilium' initialized with 1 processor(s), filter: 'ciliumEndpointName:*'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'otel-http' initialized with 9 processor(s), filter: 'Attributes.http.method:*'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'otel-messaging' initialized with 8 processor(s), filter: 'Attributes.messaging.system:*'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'otel-exceptions' initialized with 5 processor(s), filter: 'Attributes.exception.type:*'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'otel-generic' initialized with 3 processor(s), filter: 'none'
info: TransformationPipeline.Services.TransformationService[0]
Pipeline 'unknown-fallback' initialized with 1 processor(s), filter: 'none'
info: TransformationPipeline.Services.TransformationService[0]
TransformationService initialized successfully with 14 pipelines
info: TransformationPipeline.Services.PipelineInitializer[0]
All pipelines initialized successfully.`;
const chunkSizes = [500, 600, 700, 800, 1100, 1200, 1300, 1400, 1500, 1600, 1650, 1700, 1800, 1900, 2000, 5000, 10000, 15000, logContent.length];
console.log(`Total Log Content Length: ${logContent.length} characters`);
console.log('Chars\t| Old (ms)\t| Proposed (ms)\t| Match Equal');
console.log('----------------------------------------------------------------');
(async () => {
for (const size of chunkSizes) {
if (size > logContent.length && size !== logContent.length) continue;
const chunk = logContent.substring(0, size) + "X";
const resultOld = await runRegexInWorker('old', chunk);
const resultProp = await runRegexInWorker('proposed', chunk);
const timeOld = resultOld.time;
const timeProp = resultProp.time;
// Compare matches (boolean result of .test())
// Note: Since we append 'X' to force failure/backtracking, both should return false.
// But let's verify they behave identically.
const equal = (resultOld.match === resultProp.match) ? "YES" : "NO";
console.log(`${size}\t| ${timeOld}\t| ${timeProp}\t\t| ${equal}`);
}
})();
function runRegexInWorker(regexName, input) {
return new Promise((resolve) => {
const worker = new Worker(__filename, {
workerData: { regexName, input }
});
const timeout = setTimeout(() => {
worker.terminate();
resolve({ time: 'TIMEOUT', match: 'N/A' });
}, 5000); // 5 seconds timeout
worker.on('message', (msg) => {
clearTimeout(timeout);
resolve(msg);
});
worker.on('error', (err) => {
clearTimeout(timeout);
resolve({ time: 'ERROR', match: 'N/A' });
});
worker.on('exit', (code) => {
if (code !== 0) {
clearTimeout(timeout);
}
});
});
}
} else {
const { performance } = require('perf_hooks');
const { regexName, input } = workerData;
const regexes = {
old: /\s*(?:\[[^\]]\]\s+[^\[\]]+\s*)+(?:\(default is\s+"[^"]+"\):)?\s+$/,
proposed: /\s*(?:\[[^\]]\]\s+[^\[\s][^\[]*\s*)+(?:\(default is\s+"[^"]+"\):)?\s+$/
};
const regex = regexes[regexName];
try {
const start = performance.now();
const match = regex.test(input);
const end = performance.now();
parentPort.postMessage({
time: (end - start).toFixed(4),
match: match
});
} catch (e) {
// Should not happen for regex test
}
} |
Tyriar
approved these changes
Dec 11, 2025
Terminal: Fix ReDoS in PowerShell prompt detection Refactors the PowerShell confirmation regex to prevent catastrophic backtracking (ReDoS). The previous pattern `[^\[]+` implicitly matched whitespace, creating an overlap with the outer loop's `\s+`. This caused exponential complexity when processing strings with many spaces not followed by a valid bracket. The new pattern `(?:[^\[\s]|\s+(?!\[))+` enforces mutual exclusion between content and separators, ensuring linear performance. Fixes microsoft#279842
initial proposed fix is false, here's the fixed fix
auto-merge was automatically disabled
December 11, 2025 18:36
Head branch was pushed to by a user without write access
Contributor
Author
|
i've rebased the branch since the CI was failing (https://github.com/microsoft/vscode/actions/runs/19782951892/job/57814695787) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #279842
Description
This PR fixes a ReDoS (Regular Expression Denial of Service) vulnerability in the PowerShell prompt detection regex located in
outputMonitor.ts.The Issue:
The original regex
\s*(?:\[[^\]]\]\s+[^\[]+\s*)+...suffers from catastrophic backtracking due to ambiguity between the separator\s+and the content matcher[^\[]+.Since
[^\[]+matches any character that isn't[(including whitespace), a long sequence of indented lines (common in structured logs like .NET) causes the engine to explore exponential combinations of how to split the whitespace between the separator and the content.The Fix:
I have optimized the regex to strictly enforce the boundary between the separator and the content:
[^\[]+(Allowed content to start with whitespace, creating overlap with the preceding\s+)[^\[\s][^\[]*This change enforces that the "content" part of the prompt pattern must start with a non-whitespace character. This eliminates the ambiguity: any whitespace following the
[...]block is now greedily consumed by the explicit\s+separator and cannot be "claimed" by the content matcher.Performance Verification:
Tested against a 16KB log file containing indented .NET logs that previously caused a terminal freeze:
The complexity is reduced from exponential to linear O(n).