Time Of Check Time Of Use in Adonisjs with Dynamodb
Time Of Check Time Of Use in Adonisjs with Dynamodb — 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 AdonisJS applications that interact with DynamoDB, this commonly manifests in workflows where an authorization check is performed, followed later by a database operation that assumes the check is still valid.
Consider a typical user profile update route in AdonisJS. The controller may first verify that the requesting user owns a given record by comparing the authenticated user ID to a record’s owner attribute fetched from DynamoDB. If this check is performed in one request and the subsequent write occurs in a separate request or step, an attacker can change their identity or permissions between the check and the use. Because DynamoDB is a remote, eventually consistent data store, there is no built-in locking mechanism to guarantee the record has not changed between the read used for authorization and the write operation.
An illustrative vulnerable pattern involves an unauthenticated or weakly authenticated endpoint that exposes a user ID parameter. AdonisJS code might retrieve the current user’s data via a DynamoDB get, perform a permission comparison, and then pass the same user-supplied identifier to a subsequent update call. If an attacker can manipulate the identifier between the get and the update (for example, by altering a JWT payload between issuance and use, or by chaining requests to escalate privileges), they may execute actions on other users’ resources. This maps to common API risks such as BOLA/IDOR and can be discovered by checks like BOLA/IDOR and Property Authorization in middleBrick scans.
The risk is compounded when the application uses unauthenticated endpoints or token-based authentication without proper scope validation. In such cases, an attacker can probe endpoints to identify patterns where checks are separated from mutations. middleBrick’s unauthenticated scan capability surfaces these attack surfaces by testing the exposed endpoints without credentials, highlighting routes where authorization checks do not tightly guard subsequent DynamoDB operations.
Additionally, DynamoDB’s conditional writes provide a mitigation primitive, but developers must explicitly implement them. Without conditional expressions that re-validate ownership or state at write time, the window between check and use remains exploitable. This is especially relevant for operations that involve counters, flags, or multi-step workflows where the application state could be altered by other processes or compromised tokens.
In summary, the combination of AdonisJS request handling, asynchronous or separate authorization checks, and DynamoDB’s remote data model creates opportunities for TOCTOU when the application fails to enforce integrity and freshness of decisions at the point of use. Mitigations require designing authorization logic that is either atomic or strongly validated immediately before each write, leveraging conditional writes, and ensuring that identity and permissions are verified as close as possible to the operation itself.
Dynamodb-Specific Remediation in Adonisjs — concrete code fixes
To address TOCTOU in AdonisJS applications using DynamoDB, enforce authorization and state validation at the moment of each write. Use DynamoDB conditional expressions so that the update fails if the record’s state has changed since the last read. Combine this with tightly scoped tokens and server-side identity checks to ensure the subject of the request cannot be tampered with between check and use.
Example: Safe profile update with conditional write
In this example, an AdonisJS controller updates a user profile only if the item’s owner ID still matches the authenticated user. The conditional expression acts as a runtime check immediately before the write, closing the TOCTOU gap.
import { DynamoDBClient, UpdateItemCommand } from "@aws-sdk/client-dynamodb";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
const client = new DynamoDBClient({ region: "us-east-1" });
export default class ProfileController {
async updateProfile({ request, auth }) {
const userId = auth.user.id; // authenticated subject from token/session
const { displayName } = request.all();
const tableName = "users";
const userIdValue = { S: userId };
const cmd = new UpdateItemCommand({
TableName: tableName,
Key: marshall({ id: { S: userId } }),
UpdateExpression: "SET #dn = :val",
ConditionExpression: "#uid = :uid AND attribute_exists(#dn)",
ExpressionAttributeNames: {
"#uid": "ownerId",
"#dn": "displayName"
},
ExpressionAttributeValues: marshall({
":uid": userIdValue,
":val": { S: displayName }
})
});
try {
const response = await client.send(cmd);
return { updated: true, data: unmarshall(response.Attributes) };
} catch (err) {
if (err.name === "ConditionalCheckFailedException") {
throw new Error("Conflict: the record was modified or access is invalid.");
}
throw err;
}
}
}
The ConditionExpression re-validates that the ownerId matches the authenticated user and that the attribute exists at the moment of writing. If another request or process modifies the item between the read and the write, the conditional write fails, preventing unauthorized updates.
Example: Idempotent counter update with versioning
When working with mutable data such as counters, include a version attribute or use the item’s updated_at timestamp as part of the condition. This ensures that concurrent modifications are detected and rejected or merged safely.
import { DynamoDBClient, UpdateItemCommand } from "@aws-sdk/client-dynamodb";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
const client = new DynamoDBClient({ region: "us-east-1" });
export default class CounterController {
async increment({ request, auth }) {
const userId = auth.user.id;
const tableName = "counters";
const cmd = new UpdateItemCommand({
TableName: tableName,
Key: marshall({ userId: { S: userId } }),
UpdateExpression: "SET #val = if_not_exists(#val, :zero) + :inc, #ver = #ver + :one",
ConditionExpression: "#ver = :expectedVer",
ExpressionAttributeNames: {
"#val": "value",
"#ver": "version"
},
ExpressionAttributeValues: marshall({
":inc": { N: "1" },
":zero": { N: "0" },
":one": { N: "1" },
":expectedVer": { N: request.input().expectedVersion || "0" }
})
});
try {
const response = await client.send(cmd);
return unmarshall(response.Attributes);
} catch (err) {
if (err.name === "ConditionalCheckFailedException") {
throw new Error("Version mismatch: please refresh and retry.");
}
throw err;
}
}
}
By embedding the expected version in the condition, the update only succeeds if the item has not been concurrently modified. This pattern is effective for mitigating race conditions and TOCTOU-like issues in distributed systems.
In addition to conditional writes, ensure that authorization decisions are derived from trusted sources at the time of the operation. Avoid relying on client-supplied identifiers for cross-user access checks. middleBrick scans, including the Property Authorization and BOLA/IDOR checks, can help identify endpoints where such unsafe patterns exist.
Finally, apply the principle of least privilege to DynamoDB requests by using scoped credentials and policies that limit each role to the minimum required actions on specific table patterns. This reduces the impact of any token compromise and complements the runtime guards implemented through conditional expressions.