require-stream-error-handler
Require an error listener on streams passed to pipe, which does not forward errors
Detects .pipe() on a stream that has no 'error' listener. This rule is part of eslint-plugin-node-security and provides LLM-optimized error messages with fix suggestions.
🚨 Security rule | 💡 Provides suggestions | ⚠️ Set to error in recommended
Quick Summary
| Aspect | Details |
|---|---|
| CWE Reference | CWE-248 (Uncaught Exception) |
| Severity | High (remote denial of service) |
| Auto-Fix | 💡 Suggests a listener or pipeline() |
| Category | Security |
| ESLint MCP | ✅ Optimized for ESLint MCP integration |
| Best For | HTTP servers that stream files, uploads, or compressed bodies |
Vulnerability and Risk
Vulnerability: .pipe() forwards data and nothing else. It does not forward errors, and it does not destroy the source when the destination fails. A stream that emits 'error' with no listener re-throws inside the EventEmitter, and in Node an unhandled 'error' event is an uncaught exception — the process exits.
Risk: One request for a missing, unreadable, or permission-denied file is enough to stop the server. That makes it a remote denial of service costing the attacker a single request, with no authentication and no payload.
Rule Details
The rule reports a .pipe() whose source or destination is a stream it can prove has no handler:
- Constructed inline —
fs.createReadStream(p).pipe(res). The value has no name, so no'error'listener can ever have been attached to it. This is a property of the expression, not a heuristic. - Named but never handled — a name bound to a stream constructor that never appears with
.on('error'),.once('error'), or.addListener('error')anywhere in the file.
The whole file is judged at Program:exit, so a listener registered after the .pipe() still counts. Statement order is not the criterion.
pipeline() is never reported. It destroys every stream and surfaces the failure through its callback or rejected promise, which is exactly the fix this rule recommends — reporting it would be reporting the mitigation.
Examples
❌ Incorrect
import fs from 'fs';
// Constructed inline — nothing can have listened to it
function download(req, res) {
fs.createReadStream(`./uploads/${req.params.id}`).pipe(res);
}
// The DESTINATION is constructed inline; a disk error is equally fatal
busboy.on('file', (name, file) => {
file.pipe(fs.createWriteStream(`./tmp/${name}`));
});
// Named, resolvable, never handled
const stream = fs.createReadStream('/etc/hosts');
stream.pipe(res);✅ Correct
import fs from 'fs';
import { pipeline } from 'stream/promises';
// Name it, handle 'error', then pipe
function download(req, res) {
const stream = fs.createReadStream(`./uploads/${req.params.id}`);
stream.on('error', () => {
if (!res.headersSent) res.status(404).end();
});
stream.pipe(res);
}
// Or use pipeline(), which destroys every stream and reports the failure
async function downloadSafely(req, res) {
try {
await pipeline(fs.createReadStream(`./uploads/${req.params.id}`), res);
} catch {
if (!res.headersSent) res.status(404).end();
}
}Configuration
| Option | Type | Default | Description |
|---|---|---|---|
allowInTests | boolean | true | Allow unhandled stream errors in test files |
{
rules: {
'node-security/require-stream-error-handler': ['error', {
allowInTests: true
}]
}
}Security Impact
| Vulnerability | CWE | OWASP | CVSS | Impact |
|---|---|---|---|---|
| Uncaught Exception | 248 | A04:2021 | 7.5 High | Process exit — remote denial of service |
| Improper Error Handling | 391 | A04:2021 | 5.3 Med | Partial writes, truncated responses |
Related Rules
no-unbounded-decompression— Detect decompression with no output limitdetect-non-literal-fs-filename— Detect attacker-steerable filesystem paths
Known False Negatives
Handlers attached in another module
Why: A bare identifier with no visible binding may well be handled by the code that created it. "I could not prove this is handled" is not a finding.
// ❌ NOT DETECTED — provenance is outside this file
export function forward(incoming, outgoing) {
incoming.pipe(outgoing);
}Mitigation: Attach the listener where the stream is created, or use pipeline() at the boundary.
Handlers registered through a helper
Why: The rule looks for a literal 'error' event name on the stream's own name.
// ❌ NOT DETECTED
const s = fs.createReadStream(p);
attachStandardHandlers(s);
s.pipe(res);Mitigation: Prefer pipeline(), which needs no listener bookkeeping at all.
Further Reading
- readable.pipe() — why
pipedoes not forward errors - stream.pipeline() — the error-propagating replacement
- CWE-248: Uncaught Exception — Official CWE entry
⚙️ Options
| Option | Type | Default | Description |
|---|---|---|---|
allowInTests | boolean | true | Allow unhandled stream errors in test files |
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.