no-toctou-vulnerability
Detects Time-of-Check-Time-of-Use (TOCTOU) race condition vulnerabilities in file system operations.
Quick Summary
| Aspect | Details |
|---|---|
| Severity | Error |
| CWE | CWE-367 |
| OWASP | A01:2021 |
| Auto-Fix | ❌ No |
| Suggestions | ✅ 3 available |
| Category | Security |
Vulnerability and Risk
Vulnerability: A Time-of-Check to Time-of-Use (TOCTOU) vulnerability relies on the timing window between checking a condition (e.g., "does this file exist?") and using the result (e.g., "read the file").
Risk: An attacker can alter the state of the system (e.g., delete or replace the file with a symbolic link) during this window, causing the application to perform actions on the wrong resource. This can lead to privilege escalation, data corruption, or denial of service.
Rule Details
TOCTOU vulnerabilities occur when a program checks a condition (like file existence) and later performs an action based on that check, but the condition may have changed between the check and the action.
A race needs somewhere for the attacker to stand
Winning a TOCTOU race means creating or replacing the name between the check
and the use, and that requires write permission on the containing
directory. In /tmp — a namespace every local account can write — that is
trivial. Under $HOME, $XDG_CACHE_HOME or %LOCALAPPDATA% no other
unprivileged user can create the name at all, and the process is already
running as the user whose directory it is, so there is no privilege transition
either.
The rule therefore stays silent when the path resolves to a per-user root —
os.homedir(), os.userInfo().homedir, or one of HOME, USERPROFILE,
LOCALAPPDATA, APPDATA, XDG_{CACHE,CONFIG,DATA,STATE}_HOME. The resolution
follows const bindings, a file-local helper's single top-level return,
platform ternaries, ?? fallbacks and path.join, which is how a cache path is
normally assembled:
// Not reported — Shopify/cli bin/update-observe.js:402
function cookieCachePath() {
const cacheRoot =
platform() === 'darwin'
? join(homedir(), 'Library', 'Caches')
: process.env.XDG_CACHE_HOME ?? join(homedir(), '.cache');
return join(cacheRoot, 'shopify-cli', 'observe-cookie');
}
function clearCachedCookie() {
const p = cookieCachePath();
if (existsSync(p)) unlinkSync(p); // no shared namespace, no window
}The exemption is scoped to the absence of a shared namespace, not to the absence of a race in general. A per-user path reached through a directory an attacker can already write is out of scope for any rule that does not resolve the filesystem.
Why This Matters
| Issue | Impact | Solution |
|---|---|---|
| 🔒 Race Condition | Attackers can exploit timing window | Use atomic operations |
| 🐛 Data Integrity | File state may change unexpectedly | Use fs.promises API |
| ⚡ Reliability | Operations may fail or produce wrong results | Handle errors properly |
Configuration
interface Options {
/** Ignore in test files. Default: true */
ignoreInTests?: boolean;
/** File system methods to check. Default: ['fs.existsSync', 'fs.statSync', 'fs.accessSync'] */
fsMethods?: string[];
}Examples
❌ Incorrect
// TOCTOU vulnerability: file state can change between check and use
if (fs.existsSync('config.json')) {
const data = fs.readFileSync('config.json'); // ⚠️ File may no longer exist
}
// Stat-then-use pattern
const stats = fs.statSync('file.txt');
if (stats.isFile()) {
fs.unlinkSync('file.txt'); // ⚠️ File may have changed
}✅ Correct
// Use try-catch with fs.promises (atomic behavior)
try {
const data = await fs.promises.readFile('config.json');
// Process data
} catch (err) {
if (err.code === 'ENOENT') {
// File doesn't exist - handle appropriately
}
}
// Use proper locking for concurrent access
import { lock } from 'proper-lockfile';
const release = await lock('file.txt');
try {
const data = await fs.promises.readFile('file.txt');
await fs.promises.writeFile('file.txt', modifiedData);
} finally {
await release();
}Configuration Examples
Basic Usage
// eslint.config.mjs
export default [
{
rules: {
'node-security/no-toctou-vulnerability': 'error',
},
},
];Custom Configuration
// eslint.config.mjs
export default [
{
rules: {
'node-security/no-toctou-vulnerability': [
'error',
{
ignoreInTests: true,
fsMethods: ['fs.existsSync', 'fs.statSync', 'fs.accessSync'],
},
],
},
},
];LLM-Optimized Output
🔒 CWE-367 OWASP:A01-Broken-Access-Control CVSS:7.0 | TOCTOU vulnerability | HIGH
Fix: Use atomic operations or fs.promises for file operations | https://cwe.mitre.org/data/definitions/367.htmlSuggestions
When this rule reports an error, it provides three suggestions:
- Use Atomic Operations - Use atomic file operations like
fs.promises.access()thenfs.promises.readFile() - Use fs.promises - Use the Promise-based fs API for better error handling
- Add Proper Locking - Use a file locking library like
proper-lockfilefor concurrent access
Related Rules
detect-non-literal-fs-filename- Detects path traversalno-zip-slip- Prevents zip slip vulnerabilities
Known False Negatives
The following patterns are not detected due to static analysis limitations:
A per-user path an attacker can already reach
The per-user exemption above assumes the resolved root really is private. A
world-writable $HOME, a shared service account, or a setuid binary running
with a different real and effective user all break that assumption, and the
rule reports nothing in those cases.
Values from Variables
Why: Values stored in variables are not traced.
// ❌ NOT DETECTED - Value from variable
const value = userInput;
dangerousOperation(value);Mitigation: Validate all user inputs.
Wrapper Functions
Why: Custom wrappers not recognized.
// ❌ NOT DETECTED - Wrapper
myWrapper(userInput); // Uses dangerous API internallyMitigation: Apply rule to wrapper implementations.
Dynamic Invocation
Why: Dynamic calls not analyzed.
// ❌ NOT DETECTED - Dynamic
obj[method](userInput);Mitigation: Avoid dynamic method invocation.
Further Reading
- CWE-367: TOCTOU Race Condition - Official CWE documentation
- OWASP TOCTOU - OWASP vulnerability documentation
- Node.js fs.promises - Recommended async file API
⚙️ Options
| Option | Type | Default | Description |
|---|---|---|---|
ignoreInTests | boolean | true | Skip this rule in *.test.* / *.spec.* files |
fsMethods | string[] | ["fs.existsSync","fs.statSync","fs.accessSync","fs.exists","fs.stat","fs.access"] | Filesystem check calls that create a time-of-check window. Replaces the built-in list. Only the final dotted segment is compared, so fs.existsSync and existsSync are the same entry. |
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.