Time Of Check Time Of Use in Express with Basic Auth
Time Of Check Time Of Use in Express with Basic Auth — how this specific combination creates or exposes the vulnerability
Time Of Check Time Of Use (TOCTOU) occurs when the outcome of a security decision depends on the timing between a check and the subsequent use of a resource. In Express applications that use HTTP Basic Auth, this commonly manifests when authorization checks are performed separately from the actual resource access, creating a window where state can change.
Consider an endpoint that first validates credentials and then reads a user-controlled identifier to fetch a resource. An attacker can exploit this by changing the resource identifier between the authorization check and the file or data access. Because Basic Auth credentials are decoded from a base64-encoded header on each request, the authentication itself is stateless; the vulnerability arises in how the application links authentication to authorization and resource handling.
For example, an application might authenticate a user, verify that the user has permission to view a document, and then use a filename from user input to read from disk. If the filename is mutable between the permission check and the filesystem read, an authenticated user could leverage a privileged file path (such as /etc/passwd) by altering the request between the check and the read. This is especially relevant when endpoints perform a lightweight existence or ownership check using one request parameter and then open a different resource based on a second parameter that can be manipulated later.
An attacker may also chain this with other patterns such as BOLA/IDOR by authenticating as a low-privilege account, passing an initial check, then changing an ID in a subsequent call to access another user’s data. In an Express route, this can look like authenticating once, storing a decoded user payload in the request, and then trusting an object ID from the URL or body without re-verifying scope on each operation.
Because middleBrick scans the unauthenticated attack surface, it can flag endpoints where authentication headers are accepted but the route logic does not enforce strict context-bound authorization on every use. The scanner’s checks for Authentication and BOLA/IDOR, combined with Property Authorization and Input Validation, are designed to surface these mismatches. Findings include evidence such as mismatched authorization decisions and untrusted input used in sensitive operations, alongside remediation guidance to bind access checks closely to the point of use.
Basic Auth-Specific Remediation in Express — concrete code fixes
To mitigate TOCTOU in Express with Basic Auth, ensure that authorization is re-evaluated immediately before any sensitive action and that all inputs are validated and sanitized in a single, atomic step. Avoid caching authorization decisions across multiple operations or requests, and do not trust decoded user information beyond the scope of a single, well-contained handler.
Use middleware to parse and validate credentials on each request, then perform authorization checks inline with the resource operation. This keeps the check and use tightly coupled and reduces the window for state manipulation. Below are concrete, working examples demonstrating secure patterns.
Insecure pattern to avoid:
// Insecure: authorization check separated from file read
app.get('/documents/:id', (req, res) => {
const user = authenticateBasic(req); // decodes auth header
if (!user) return res.sendStatus(401);
// Perform a permission check (e.g., check DB or object ownership)
const hasAccess = checkDocumentAccess(user.id, req.params.id);
if (!hasAccess) return res.sendStatus(403);
// TOCTOU risk: attacker can change behavior between check and read
const filePath = getUserFile(req.params.id); // uses same ID but may be mutable
fs.readFile(filePath, (err, data) => {
if (err) return res.sendStatus(500);
res.send(data);
});
});
Secure pattern with immediate authorization and strict input validation:
const express = require('express');
const cors = require('cors');
const basicAuth = require('express-basic-auth');
const fs = require('fs').promises;
const path = require('path');
const app = express();
app.use(cors());
app.use(express.json());
// Configure Basic Auth with a user store and optional challenge
app.use(basicAuth({
users: { alice: 'p4ssw0rd', bob: 's3cret' },
challenge: true,
realm: 'Documents API',
}));
// Secure route: bind authorization to the resource operation
app.get('/documents/:id', async (req, res) => {
// The auth middleware attaches req.auth; ensure it exists
if (!req.auth || !req.auth.user) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Validate and sanitize input early
const documentId = req.params.id;
if (!/^[a-f0-9-]{36}$/i.test(documentId)) {
return res.status(400).json({ error: 'Invalid document identifier' });
}
// Re-evaluate authorization immediately before use
const ownership = await getDocumentOwnership(documentId);
if (!ownership || ownership.userId !== req.auth.user) {
return res.status(403).json({ error: 'Forbidden' });
}
// Construct file path within a controlled directory using the validated ID
const baseDir = path.resolve(__dirname, 'documents');
const filePath = path.join(baseDir, `${documentId}.txt`);
// Ensure the resolved path remains within baseDir (path traversal defense)
if (!filePath.startsWith(baseDir + path.sep)) {
return res.status(403).json({ error: 'Forbidden path' });
}
try {
const data = await fs.readFile(filePath, 'utf8');
res.json({ id: documentId, content: data });
} catch (err) {
if (err.code === 'ENOENT') {
return res.status(404).json({ error: 'Not found' });
}
res.status(500).json({ error: 'Internal server error' });
}
});
async function getDocumentOwnership(documentId) {
// Simulated DB lookup; in practice, query with parameterized inputs
const map = {
'a1b2c3d4-e5f6-7890-abcd-ef1234567890': { userId: 'alice' },
'b2c3d4e5-f6a7-8901-bcde-f12345678901': { userId: 'bob' },
};
return map[documentId] || null;
}
app.listen(3000, () => console.log('Server running on port 3000'));
Key practices reflected in the secure example:
- Authenticate on each request and do not rely on cached authorization state across operations.
- Validate and sanitize all inputs before use, using strict patterns for identifiers.
- Perform authorization immediately before the sensitive action (file read), using the same validated identifiers.
- Constrain file paths to a controlled directory and reject any traversal attempts to prevent path manipulation attacks.
These measures reduce the TOCTOU window by ensuring the decision to access a resource is made as close as possible to the access itself and that inputs remain immutable between validation and use.