Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions convex/convex/githubApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function base64UrlEncodeString(str: string): string {

const REQUIRED_ENV_VARS = [
"GITHUB_APP_ID",
"GITHUB_APP_PRIVATE_KEY",
"GITHUB_APP_PRIVATE_KEY_BASE64",
"GITHUB_APP_CLIENT_ID",
"GITHUB_APP_CLIENT_SECRET",
] as const;
Expand All @@ -47,9 +47,13 @@ export function requireGitHubAppEnv(): GitHubAppEnv {
if (missing.length > 0) {
throw new Error(`Missing GitHub App configuration: ${missing.join(", ")}`);
}

// Decode the Base64 string back into the strict multiline PEM format required by OpenSSL
const formattedPrivateKey = atob(process.env.GITHUB_APP_PRIVATE_KEY_BASE64!);

return {
appId: process.env.GITHUB_APP_ID!,
privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
privateKey: formattedPrivateKey,
clientId: process.env.GITHUB_APP_CLIENT_ID!,
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
};
Expand Down Expand Up @@ -79,9 +83,8 @@ export async function generateAppJwt(

const signingInput = `${header}.${payload}`;

// Normalize PEM: replace literal \n with real newlines, strip headers, decode
const normalizedPem = privateKeyPem.replace(/\\n/g, "\n");
const pemBody = normalizedPem
// Strip PEM headers and whitespace to get raw base64 DER
const pemBody = privateKeyPem
.replace(/-----BEGIN (?:RSA )?PRIVATE KEY-----/g, "")
.replace(/-----END (?:RSA )?PRIVATE KEY-----/g, "")
.replace(/\s/g, "");
Expand Down
15 changes: 8 additions & 7 deletions convex/convex/githubAppNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function base64UrlEncode(data: Buffer): string {

const REQUIRED_ENV_VARS = [
"GITHUB_APP_ID",
"GITHUB_APP_PRIVATE_KEY",
"GITHUB_APP_PRIVATE_KEY_BASE64",
"GITHUB_APP_CLIENT_ID",
"GITHUB_APP_CLIENT_SECRET",
] as const;
Expand All @@ -33,9 +33,14 @@ export function requireGitHubAppEnv(): GitHubAppEnv {
if (missing.length > 0) {
throw new Error(`Missing GitHub App configuration: ${missing.join(", ")}`);
}

const base64Key = process.env.GITHUB_APP_PRIVATE_KEY_BASE64!;
// Decode the Base64 string back into the strict multiline PEM format required by OpenSSL
const formattedPrivateKey = Buffer.from(base64Key, "base64").toString("ascii");

return {
appId: process.env.GITHUB_APP_ID!,
privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
privateKey: formattedPrivateKey,
clientId: process.env.GITHUB_APP_CLIENT_ID!,
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
};
Expand All @@ -55,13 +60,9 @@ export function generateAppJwt(appId: string, privateKeyPem: string): string {

const signingInput = `${header}.${payload}`;

// Handle stringified newlines from env var storage
const rawKey = privateKeyPem;
const formattedPrivateKey = rawKey.replace(/\\n/g, "\n");

const sign = crypto.createSign("RSA-SHA256");
sign.update(signingInput);
const signature = sign.sign(formattedPrivateKey);
const signature = sign.sign(privateKeyPem);

return `${signingInput}.${base64UrlEncode(signature)}`;
}
Expand Down
12 changes: 8 additions & 4 deletions convex/convex/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ function ghBase64UrlEncodeString(str: string): string {

const GH_REQUIRED_ENV_VARS = [
"GITHUB_APP_ID",
"GITHUB_APP_PRIVATE_KEY",
"GITHUB_APP_PRIVATE_KEY_BASE64",
"GITHUB_APP_CLIENT_ID",
"GITHUB_APP_CLIENT_SECRET",
] as const;
Expand All @@ -894,9 +894,13 @@ function requireGitHubAppEnv() {
if (missing.length > 0) {
throw new Error(`Missing GitHub App configuration: ${missing.join(", ")}`);
}

// Decode the Base64 string back into the strict multiline PEM format required by OpenSSL
const formattedPrivateKey = atob(process.env.GITHUB_APP_PRIVATE_KEY_BASE64!);

return {
appId: process.env.GITHUB_APP_ID!,
privateKey: process.env.GITHUB_APP_PRIVATE_KEY!,
privateKey: formattedPrivateKey,
clientId: process.env.GITHUB_APP_CLIENT_ID!,
clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!,
};
Expand All @@ -908,8 +912,8 @@ async function generateAppJwt(appId: string, privateKeyPem: string): Promise<str
const payload = ghBase64UrlEncodeString(JSON.stringify({ iss: appId, iat: now - 60, exp: now + 600 }));
const signingInput = `${header}.${payload}`;

const normalizedPem = privateKeyPem.replace(/\\n/g, "\n");
const pemBody = normalizedPem
// Strip PEM headers and whitespace to get raw base64 DER
const pemBody = privateKeyPem
.replace(/-----BEGIN (?:RSA )?PRIVATE KEY-----/g, "")
.replace(/-----END (?:RSA )?PRIVATE KEY-----/g, "")
.replace(/\s/g, "");
Expand Down
18 changes: 18 additions & 0 deletions test-trigger/vulnerable-sample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Test file to trigger PR scan and verify the atob fix
const express = require("express");
const app = express();

// Intentional SQL injection vulnerability for scan testing
app.get("/users", (req, res) => {
const userId = req.query.id;
const query = "SELECT * FROM users WHERE id = " + userId;
db.query(query, (err, results) => {
res.json(results);
});
});

// Intentional XSS vulnerability for scan testing
app.get("/search", (req, res) => {
const term = req.query.q;
res.send("<h1>Results for: " + term + "</h1>");
});
Loading