detached c31e89e

fix(security): use constant-time comparison for Convex API secret

commit
c31e89e21ca58dcf6b6bc8989c8dfd737d8640a4
author
Kit · · 2026-02-16 11:01 UTC
parent
1a6a567
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
1 file · +22 -1
M src/convex/http.ts +22 -1
@@ -5,6 +5,23 @@ import type { Id } from './_generated/dataModel';
55
66 const http = httpRouter();
77
8+// Constant-time string comparison to prevent timing attacks
9+function constantTimeCompare(a: string, b: string): boolean {
10+ if (a.length !== b.length) {
11+ // Still do work to avoid leaking length via timing
12+ let result = a.length ^ b.length;
13+ for (let i = 0; i < a.length; i++) {
14+ result |= a.charCodeAt(i) ^ a.charCodeAt(i);
15+ }
16+ return result === 0; // always false when lengths differ
17+ }
18+ let result = 0;
19+ for (let i = 0; i < a.length; i++) {
20+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
21+ }
22+ return result === 0;
23+}
24+
825 // Validate the secret header for server-to-server calls
926 function validateSecret(request: Request): boolean {
1027 const secret = request.headers.get('X-Convex-Secret');
@@ -15,7 +32,11 @@ function validateSecret(request: Request): boolean {
1532 return false;
1633 }
1734
18- return secret === expectedSecret;
35+ if (!secret) {
36+ return false;
37+ }
38+
39+ return constantTimeCompare(secret, expectedSecret);
1940 }
2041
2142 // Generic error response
@@ -5,6 +5,23 @@ import type { Id } from './_generated/dataModel';
5 5
6 const http = httpRouter();6 const http = httpRouter();
7 7
8+// Constant-time string comparison to prevent timing attacks
9+function constantTimeCompare(a: string, b: string): boolean {
10+ if (a.length !== b.length) {
11+ // Still do work to avoid leaking length via timing
12+ let result = a.length ^ b.length;
13+ for (let i = 0; i < a.length; i++) {
14+ result |= a.charCodeAt(i) ^ a.charCodeAt(i);
15+ }
16+ return result === 0; // always false when lengths differ
17+ }
18+ let result = 0;
19+ for (let i = 0; i < a.length; i++) {
20+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
21+ }
22+ return result === 0;
23+}
24+
8 // Validate the secret header for server-to-server calls25 // Validate the secret header for server-to-server calls
9 function validateSecret(request: Request): boolean {26 function validateSecret(request: Request): boolean {
10 const secret = request.headers.get('X-Convex-Secret');27 const secret = request.headers.get('X-Convex-Secret');
@@ -15,7 +32,11 @@ function validateSecret(request: Request): boolean {
15 return false;32 return false;
16 }33 }
17 34
18- return secret === expectedSecret;35+ if (!secret) {
36+ return false;
37+ }
38+
39+ return constantTimeCompare(secret, expectedSecret);
19 }40 }
20 41
21 // Generic error response42 // Generic error response

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close