Skip to content

Commit ef4987b

Browse files
committed
merge: incorporate apiBase validation security fix
2 parents 03a8ea8 + 5f79f0e commit ef4987b

File tree

2 files changed

+61
-0
lines changed

2 files changed

+61
-0
lines changed

lib/vibe-auth.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
verboseLog,
1717
fetchJson,
1818
isLikelyClerkTokenProblem,
19+
validateApiBase,
1920
} from "./vibe-utils.js";
2021

2122
// =============================================================================
@@ -31,6 +32,10 @@ const [vibeAuthErrors] = errorCauses({
3132
code: "AUTH_EXPIRED",
3233
message: "Token expired and refresh failed",
3334
},
35+
SecurityBlockError: {
36+
code: "SECURITY_BLOCK",
37+
message: "Security validation failed",
38+
},
3439
ConfigReadError: {
3540
code: "CONFIG_READ_ERROR",
3641
message: "Failed to read configuration file",
@@ -56,6 +61,7 @@ const {
5661
ConfigWriteError,
5762
TokenExchangeError,
5863
RefreshError,
64+
SecurityBlockError,
5965
} = vibeAuthErrors;
6066

6167
// =============================================================================
@@ -715,6 +721,17 @@ const discoverOidc = async (issuer) => {
715721
* @returns {Promise<object>} Vibecodr token response
716722
*/
717723
const exchangeForVibecodrToken = async ({ apiBase, clerkAccessToken }) => {
724+
// SECURITY: Validate apiBase before sending tokens to prevent credential exfiltration
725+
// A malicious or tampered apiBase could steal the Clerk access token
726+
const apiCheck = validateApiBase(apiBase);
727+
if (!apiCheck.valid) {
728+
throw createError({
729+
...SecurityBlockError,
730+
message: `Refusing to send token to untrusted API: ${apiCheck.reason}`,
731+
apiBase,
732+
});
733+
}
734+
718735
const url = `${normalizeOrigin(apiBase)}/auth/cli/exchange`;
719736
return fetchJson(url, {
720737
method: "POST",
@@ -902,6 +919,12 @@ export const refreshVibecodrToken = async ({
902919
verbose,
903920
});
904921
} catch (exchangeErr) {
922+
// SECURITY: Security errors must propagate directly - never mask them
923+
// This ensures token theft attempts are clearly reported, not hidden as auth issues
924+
if (exchangeErr?.cause?.code === "SECURITY_BLOCK") {
925+
throw exchangeErr;
926+
}
927+
905928
const now = Math.floor(Date.now() / 1000);
906929

907930
if (!clerkRefreshToken) {

lib/vibe-auth.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,44 @@ describe("refreshVibecodrToken", () => {
378378
expected: "AUTH_EXPIRED",
379379
});
380380
});
381+
382+
test("rejects malicious apiBase to prevent token exfiltration", async () => {
383+
// SECURITY: Ensure tokens are never sent to untrusted URLs
384+
fs.mkdirSync(path.dirname(tempConfigPath), { recursive: true });
385+
const testConfig = {
386+
clerk: {
387+
access_token: "clerk-token-to-protect",
388+
refresh_token: "clerk-refresh-token",
389+
expires_at: Math.floor(Date.now() / 1000) + 3600,
390+
},
391+
};
392+
fs.writeFileSync(tempConfigPath, JSON.stringify(testConfig));
393+
394+
let error;
395+
try {
396+
await refreshVibecodrToken({
397+
configPath: tempConfigPath,
398+
apiBase: "https://evil-attacker.com", // Malicious apiBase
399+
});
400+
} catch (e) {
401+
error = e;
402+
}
403+
404+
// Should reject BEFORE making any network request
405+
assert({
406+
given: "malicious apiBase",
407+
should: "throw SECURITY_BLOCK error to prevent token theft",
408+
actual: error?.cause?.code,
409+
expected: "SECURITY_BLOCK",
410+
});
411+
412+
assert({
413+
given: "malicious apiBase",
414+
should: "include reason in error message",
415+
actual: error?.message?.includes("untrusted"),
416+
expected: true,
417+
});
418+
});
381419
});
382420

383421
// =============================================================================

0 commit comments

Comments
 (0)