Side Channel Attack in Actix with Cockroachdb
Side Channel Attack in Actix with Cockroachdb — how this specific combination creates or exposes the vulnerability
A side channel attack in an Actix web service that uses CockroachDB can occur when timing differences or observable behaviors leak information about authentication, authorization, or data access patterns. In this combination, an attacker may infer the existence of a user or the structure of records based on response times, even when the API returns uniform error messages or status codes.
Actix is a high-performance Rust web framework that handles requests asynchronously. When combined with CockroachDB, a distributed SQL database, the interaction between database query latency and application-level logic can expose timing variations. For example, a login endpoint that performs a user lookup and then a separate permissions check may complete faster for non-existent users because the second step is never reached. An attacker can measure response times to infer valid user identities.
CockroachDB, while providing strong consistency and SQL semantics, does not inherently obscure query timing. If queries are structured differently based on input—such as conditional WHERE clauses or early exits in application code—execution duration may vary measurably over the network. These variations become a side channel when an attacker can send many crafted requests and observe timing differences.
Consider an endpoint that retrieves a user profile by ID. If the ID does not exist, CockroachDB may return quickly because the index lookup fails immediately, while a valid ID requires disk reads or cross-node consensus. Even if the response payloads are identical in size, the network round-trip time can differ. In a microservices environment where Actix routes requests to CockroachDB nodes, network latency can amplify these differences.
Another scenario involves authorization checks. An endpoint might first verify that a resource exists in CockroachDB and then check whether the requesting user has permission. If the existence check is performed before the permission check, an attacker without any credentials can probe for resource existence by measuring timing differences between "not found" and "forbidden" responses. This can reveal valid resource identifiers and assist in horizontal privilege escalation.
To mitigate these risks, developers must ensure that operations involving CockroachDB in Actix take constant time regardless of input or access rights. This means structuring queries and control flow so that execution paths and durations do not reveal information. Using parameterized queries and avoiding early branching based on database results are key practices. MiddleBrick can help detect such timing-related anomalies by scanning the API endpoints and analyzing response behavior across multiple requests.
Cockroachdb-Specific Remediation in Actix — concrete code fixes
Remediation focuses on making database interactions time-invariant and avoiding information leaks through timing. In Actix, this involves structuring handlers so that they perform the same sequence of operations regardless of whether a record exists or whether the user is authorized.
One approach is to always execute the same set of database operations, including dummy reads or writes, to mask timing differences. However, a simpler and more robust method is to ensure that queries are structured identically for all paths and that permission checks do not short-circuit the request flow in a way that changes query patterns.
Below is a secure example of a user profile endpoint in Actix that interacts with CockroachDB. It uses a single parameterized query to fetch both the existence and ownership of a record, avoiding early branching based on database results.
use actix_web::{web, HttpResponse, Result};
use cockroach_client::CockroachDb;
use serde::Deserialize;
#[derive(Deserialize)]
struct ProfileParams {
user_id: i32,
target_id: i32,
}
async fn get_profile(
params: web::Query,
db: web::Data,
) -> Result {
let query = "
SELECT p.username, a.permission_level
FROM users u
JOIN permissions a ON u.id = a.user_id
WHERE u.id = $1 AND a.resource_id = $2
LIMIT 1
";
let row = db.query_opt(query, &[¶ms.user_id, ¶ms.target_id]).await?;
match row {
Some(r) => {
let username: String = r.get(0);
let permission: String = r.get(1);
Ok(HttpResponse::Ok().json(json!({
"username": username,
"permission": permission
})))
}
None => {
// Return a generic response with the same shape and similar latency
Ok(HttpResponse::Ok().json(json!({
"username": "unknown",
"permission": "none"
})))
}
}
}
This pattern ensures that the query structure is consistent. The application does not perform a separate existence check before a permission check, which would create a timing difference. The database handles the join and filtering, and the application simply reads the result.
For write operations or more complex workflows, you can use a constant-time approach by always running a placeholder query when no real work is needed. For example:
async fn update_permission(
payload: web::Json,
db: web::Data,
) -> Result {
let has_record: bool = db
.query_one(
"SELECT EXISTS(SELECT 1 FROM permissions WHERE user_id = $1 AND resource_id = $2)",
&[&payload.user_id, &payload.resource_id],
)
.await?
.unwrap_or(false);
if has_record {
db.execute(
"UPDATE permissions SET level = $1 WHERE user_id = $2 AND resource_id = $3",
&[&payload.level, &payload.user_id, &payload.resource_id],
)
.await?;
} else {
// Execute a dummy no-op query to keep timing consistent
db.query_one("SELECT 1", &[]).await?;
}
Ok(HttpResponse::Ok().finish())
}
In this example, even when the record does not exist, the application performs a query, preventing timing leaks. MiddleBrick’s continuous monitoring can validate that such patterns are followed across your API surface.