Introduction
In the zeroshot-oecp Docker container build, we discovered a high-severity denial of service vulnerability lurking in the dependency tree. The culprit? The brace-expansion package at version 5.0.6, nested within @earendil-works/pi-coding-agent. This vulnerability, tracked as CVE-2026-14257, could allow attackers to craft malicious input that triggers exponential-time processing—effectively freezing the application with a single carefully constructed string.
The fix required a creative approach: since the vulnerable package was a transitive dependency (a dependency of a dependency), simply updating package.json wasn't enough. The team had to modify the Dockerfile itself to surgically replace the vulnerable nested module during the build process.
The Vulnerability Explained
What is Brace Expansion?
Brace expansion is a shell feature that generates arbitrary strings. For example, {a,b,c} expands to a b c, and {1..5} expands to 1 2 3 4 5. The brace-expansion npm package provides this functionality for Node.js applications, commonly used by glob matching libraries like minimatch and micromatch.
The Exponential-Time Problem
The vulnerability in brace-expansion versions through 5.0.7 stems from how the package handles deeply nested or complex brace patterns. When processing certain malicious inputs, the algorithm's time complexity grows exponentially with input characteristics rather than linearly.
Consider a pattern like:
{{{{{{{{{a}}}}}}}}}
Each level of nesting can multiply the processing time, and with enough nesting or specific patterns, even a relatively short input string can cause the parser to run for minutes, hours, or indefinitely.
Attack Scenario for zeroshot-oecp
In the zeroshot-oecp service, the @earendil-works/pi-coding-agent package uses brace-expansion internally. If user-controlled input reaches this parsing logic—whether through file paths, configuration strings, or API parameters—an attacker could submit a crafted payload like:
// Malicious input example
const maliciousPattern = "{".repeat(30) + "a" + "}".repeat(30);
This input, when processed by the vulnerable brace-expansion library, would cause the Node.js event loop to block while the exponential algorithm churns through combinations. The result: the entire service becomes unresponsive, denying service to legitimate users.
Real-World Impact
For a containerized service like zeroshot-oecp:
- Service Unavailability: A single malicious request could freeze the container
- Resource Exhaustion: CPU usage spikes to 100% during the attack
- Cascading Failures: In orchestrated environments, frozen containers can trigger restarts, load balancer issues, and service degradation
- No Authentication Required: If the vulnerable code path is reachable from unauthenticated endpoints, any attacker can exploit it
The Fix
The Challenge: Nested Dependencies
The vulnerable brace-expansion@5.0.6 wasn't a direct dependency—it was nested inside @earendil-works/pi-coding-agent/node_modules/. Simply running npm update brace-expansion wouldn't touch this nested copy because npm's dependency resolution had locked it to the vulnerable version within that package's subtree.
The Solution: Dockerfile Surgery
The fix modifies docker/zeroshot-oecp/Dockerfile to manually patch the nested dependency during the build:
Before:
FROM docker.io/library/node:22-bookworm-slim@sha256:f32b81066cde10a75dbac96646099533316d94bac4150c55da1636e1f0ffdc46 AS node-deps
WORKDIR /opt/node-runtime
COPY docker/zeroshot-oecp/package.json docker/zeroshot-oecp/package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund
After:
FROM docker.io/library/node:22-bookworm-slim@sha256:f32b81066cde10a75dbac96646099533316d94bac4150c55da1636e1f0ffdc46 AS node-deps
WORKDIR /opt/node-runtime
COPY docker/zeroshot-oecp/package.json docker/zeroshot-oecp/package-lock.json ./
RUN npm ci --omit=dev --ignore-scripts --no-audit --no-fund \
&& npm install brace-expansion@5.0.9 --no-save --ignore-scripts --no-audit --no-fund \
&& cp -r node_modules/brace-expansion/. node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion/ \
&& rm -rf node_modules/brace-expansion \
&& node -e "const pkg = require( \
'./node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion/package.json'); \
if (pkg.version !== '5.0.9') throw new Error('expected brace-expansion@5.0.9, got ' + pkg.version)"
Breaking Down the Fix
-
Install the patched version:
npm install brace-expansion@5.0.9 --no-savedownloads the fixed version without modifying package.json -
Copy to nested location:
cp -r node_modules/brace-expansion/. node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion/overwrites the vulnerable nested copy -
Clean up:
rm -rf node_modules/brace-expansionremoves the top-level copy since it's not needed as a direct dependency -
Verify the fix: The inline Node.js script confirms the patched version is in place, failing the build if the version doesn't match
This verification step is crucial—it ensures the fix actually worked and provides a clear error message if something goes wrong in future builds.
Prevention & Best Practices
1. Regular Dependency Auditing
Run security scans as part of your CI/CD pipeline:
# npm's built-in audit
npm audit
# Using Trivy for comprehensive scanning
trivy fs --scanners vuln .
2. Understand Your Dependency Tree
Use npm ls to visualize where packages are used:
npm ls brace-expansion
This helps identify nested dependencies that might be hiding vulnerabilities.
3. Consider Dependency Pinning Strategies
For critical applications:
- Use package-lock.json or yarn.lock consistently
- Consider using npm-shrinkwrap.json for published packages
- Implement automated dependency update tools like Dependabot or Renovate
4. Input Validation at Application Boundaries
Even with patched dependencies, validate and sanitize user input:
// Example: Limit pattern complexity before processing
function safeBraceExpand(pattern, maxLength = 100, maxNesting = 5) {
if (pattern.length > maxLength) {
throw new Error('Pattern too long');
}
const nestingDepth = (pattern.match(/{/g) || []).length;
if (nestingDepth > maxNesting) {
throw new Error('Pattern too complex');
}
return braceExpansion(pattern);
}
5. Implement Timeouts for Parsing Operations
Protect against algorithmic complexity attacks with timeouts:
const { setTimeout } = require('timers/promises');
async function parseWithTimeout(pattern, timeoutMs = 1000) {
const controller = new AbortController();
const timeout = setTimeout(timeoutMs, null, { signal: controller.signal })
.then(() => { throw new Error('Parse timeout'); });
try {
return await Promise.race([
Promise.resolve(braceExpansion(pattern)),
timeout
]);
} finally {
controller.abort();
}
}
Key Takeaways
- Nested dependencies can hide vulnerabilities: The vulnerable
brace-expansionwas buried inside@earendil-works/pi-coding-agent/node_modules/, invisible to simplenpm updatecommands - Dockerfile modifications can patch transitive dependencies: When npm's dependency resolution won't cooperate, surgical file operations during build can enforce security fixes
- Always verify security fixes: The inline
node -everification ensures the patched version is actually installed, preventing silent failures - Exponential-time complexity is a real DoS vector: Even without memory corruption or injection, algorithmic inefficiency can take down services
- Container builds should include security scanning: Tools like Trivy can catch these issues before deployment
How Orbis AppSec Detected This
- Source: User-controlled input potentially reaching glob/path matching operations in the pi-coding-agent module
- Sink:
brace-expansionparsing function withinnode_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion/ - Missing control: No version constraint ensuring brace-expansion >= 5.0.8 in the nested dependency tree
- CWE: CWE-1333 (Inefficient Regular Expression Complexity)
- Fix: Upgraded brace-expansion to 5.0.9 by modifying the Dockerfile to manually patch the nested dependency and verify the installation
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-14257 demonstrates that denial of service vulnerabilities don't require exotic exploits—sometimes an inefficient algorithm is all it takes. The brace-expansion vulnerability shows how transitive dependencies can introduce risk deep in your dependency tree, and how creative solutions like Dockerfile modifications may be necessary to patch them.
For Node.js developers, this is a reminder to:
1. Regularly audit your full dependency tree, not just direct dependencies
2. Understand where user input flows through your application
3. Implement defense-in-depth with input validation and timeouts
4. Use automated security scanning in your CI/CD pipeline
Security is an ongoing process, and staying ahead of vulnerabilities like this requires vigilance and the right tools.