bug(envd/auth): expiration check after ConstantTimeCompare leaks timing oracle in validateSigning
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 1.6k
- Forks
- 438
- PR merge metrics
- No merged PRs in 30d
Description
Summary
validateSigning in packages/envd/internal/api/auth.go performs HMAC comparison before checking signature expiration. This ordering creates a measurable timing side-channel that lets an attacker distinguish a correct-but-expired signature from an incorrect one.
Affected code
packages/envd/internal/api/auth.go lines 148-160:
// signature validation — runs first
if subtle.ConstantTimeCompare([]byte(expectedSignature), []byte(*signature)) != 1 {
return errors.New("invalid signature")
}
// signature expiration — runs second, ONLY if HMAC passed
if signatureExpiration != nil {
exp := int64(*signatureExpiration)
if exp < time.Now().Unix() {
return errors.New("signature is already expired")
}
}
Why it matters
subtle.ConstantTimeCompare is constant-time within a single invocation, but the presence or absence of the expiration check that follows is not. An attacker observing response latency can distinguish two cases:
| Request | Path through code | Relative latency |
|---|---|---|
| Wrong HMAC | ConstantTimeCompare → returns early |
shorter |
| Correct HMAC + expired | ConstantTimeCompare → expiration check → returns |
slightly longer |
This leaks a binary oracle: "your HMAC candidate is correct". Combined with the expiration timestamp already embedded in the signed string (and therefore in the request the attacker controls), this narrows a brute-force search considerably — the attacker can craft requests with a past timestamp, submit candidates, and use timing to confirm a correct HMAC without ever needing to produce a non-expired token.
The envd signing scheme is used for file download/upload URLs that are embedded in client responses. A compromised signing key lets an attacker forge arbitrary file-access requests to any running sandbox.
Fix
Move the expiration check before ConstantTimeCompare:
// Check expiration first — before HMAC — so a correct-but-expired
// signature does not produce a measurably longer response than an
// incorrect one, eliminating the timing oracle.
if signatureExpiration != nil {
exp := int64(*signatureExpiration)
if exp < time.Now().Unix() {
return errors.New("signature is already expired")
}
}
// HMAC validation (constant-time).
if subtle.ConstantTimeCompare([]byte(expectedSignature), []byte(*signature)) != 1 {
return errors.New("invalid signature")
}
Expired requests are rejected before any HMAC work is done, so timing no longer carries HMAC-validity information.
Severity
Low-medium. Exploiting this requires many precise latency measurements against a live envd endpoint, and the envd port is not externally exposed by default. However the signing key is long-lived (tied to sandbox lifetime) and file API access is unrestricted once forged, making the impact of a successful key recovery significant.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/envd/internal/api/auth.go at validateSigning and inspect the expiration and ConstantTimeCompare checks around lines 148-160. Verify that expired signatures are rejected before HMAC comparison and that incorrect signatures still return the existing error; done means the timing distinction described in the issue is removed without changing other validation behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- authentication, backend, security
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100