require OIDC verification for DB secrets on run-context (#538)
* add wiki/betterstack.md documenting log querying, request-ID grouping, and MCP usage
Made-with: Cursor
* fix webhook race conditions: separate runId assignment from data updates
the workflow_run webhook handler had a race where concurrent handlers assigned
the same runId to different pending records. the loser's P2002 silently dropped
data updates (jobId, status, completedAt). fix by splitting into two steps:
assignRunId() handles the race-safe unique assignment, then data updates always
target where: { runId } so they hit the correct record regardless of who won.
also downgrade R2 ObjectLockedByBucketPolicy errors from error to warn level
since duplicate webhook deliveries writing the same key is expected under load.
Made-with: Cursor
* require OIDC verification for DB secrets on run-context endpoint
DB secrets transported via run-context were accessible to any GitHub API
token with read access, bypassing GitHub Actions' fork PR secret isolation.
Now the endpoint requires a valid GitHub Actions OIDC token
(X-GitHub-OIDC-Token header) with a matching repository claim before
returning dbSecrets. Also requires admin for account-scope CLI secret
writes (matching the dashboard), and removes dead redactSecrets code.
Made-with: Cursor
This commit is contained in:
committed by
pullfrog[bot]
parent
a4c7c0fc15
commit
c86752cf1d
+10
-4
@@ -57,18 +57,24 @@ const defaultRunContext: RunContext = {
|
||||
export async function fetchRunContext(params: {
|
||||
token: string;
|
||||
repoContext: RepoContext;
|
||||
oidcToken?: string | undefined;
|
||||
}): Promise<RunContext> {
|
||||
const timeoutMs = 30000;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${params.token}`,
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
if (params.oidcToken) {
|
||||
headers["X-GitHub-OIDC-Token"] = params.oidcToken;
|
||||
}
|
||||
|
||||
const response = await apiFetch({
|
||||
path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as core from "@actions/core";
|
||||
import type { Octokit } from "@octokit/rest";
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
import { log } from "./cli.ts";
|
||||
@@ -32,9 +33,16 @@ export async function resolveRunContextData(
|
||||
|
||||
const repoContext = parseRepoContext();
|
||||
|
||||
let oidcToken: string | undefined;
|
||||
try {
|
||||
oidcToken = await core.getIDToken("pullfrog-api");
|
||||
} catch {
|
||||
// OIDC not available (local dev, non-actions environment, fork PRs)
|
||||
}
|
||||
|
||||
const [repoResponse, runContext] = await Promise.all([
|
||||
params.octokit.repos.get({ owner: repoContext.owner, repo: repoContext.name }),
|
||||
fetchRunContext({ token: params.token, repoContext }),
|
||||
fetchRunContext({ token: params.token, repoContext, oidcToken }),
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
+1
-39
@@ -1,10 +1,7 @@
|
||||
/**
|
||||
* Secret detection and redaction utilities
|
||||
* Redacts actual secret values rather than using pattern matching
|
||||
* Secret detection and env filtering utilities
|
||||
*/
|
||||
|
||||
import { getGitHubInstallationToken } from "./token.ts";
|
||||
|
||||
// patterns for sensitive env var names
|
||||
export const SENSITIVE_PATTERNS = [
|
||||
/_KEY$/i,
|
||||
@@ -47,38 +44,3 @@ export function resolveEnv(mode: EnvMode | undefined): Record<string, string | u
|
||||
// custom env object - merge with restricted base
|
||||
return { ...filterEnv(), ...mode };
|
||||
}
|
||||
|
||||
function getAllSecrets(): string[] {
|
||||
const secrets: string[] = [];
|
||||
|
||||
// collect all env var values matching SENSITIVE_PATTERNS
|
||||
for (const [key, value] of Object.entries(process.env)) {
|
||||
if (value && isSensitiveEnvName(key)) {
|
||||
secrets.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
// add GitHub installation token (stored in memory, not in env)
|
||||
try {
|
||||
const token = getGitHubInstallationToken();
|
||||
if (token) {
|
||||
secrets.push(token);
|
||||
}
|
||||
} catch {
|
||||
// token not set yet, ignore
|
||||
}
|
||||
|
||||
return secrets;
|
||||
}
|
||||
|
||||
export function redactSecrets(content: string, secrets?: string[]): string {
|
||||
const secretsToRedact = [...(secrets ?? []), ...getAllSecrets()];
|
||||
let redacted = content;
|
||||
for (const secret of secretsToRedact) {
|
||||
if (secret) {
|
||||
const escaped = secret.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
redacted = redacted.replaceAll(new RegExp(escaped, "g"), "[REDACTED_SECRET]");
|
||||
}
|
||||
}
|
||||
return redacted;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user