Description
In src/app/actions/profile.ts, the bootstrapProfile server action sends the user's live GitHub OAuth provider_token as a field in an Inngest event payload. Inngest stores event payloads in its own cloud infrastructure. This means every signing user's GitHub access token is persisted in a third-party service outside the application's control.
Affected File and Lines
src/app/actions/profile.ts -- bootstrapProfile function, Inngest send call
Buggy Code
// src/app/actions/profile.ts
const providerToken = (await sb.auth.getSession()).data.session?.provider_token;
if (providerToken) {
await inngest.send({
name: 'audit/run',
data: {
userId: profile.id,
githubHandle: profile.github_handle,
githubId,
accessToken: providerToken, // live OAuth token placed in event payload
},
});
auditQueued = true;
}
The audit/run Inngest function then reads this token:
// src/inngest/functions/audit-run.ts
type AuditEvent = {
data: {
userId: string;
githubHandle: string;
githubId: string;
accessToken?: string; // token stored in Inngest event log
installationId?: number;
};
};
Impact
1. Token persisted in Inngest's cloud storage: Inngest retains event payloads for replay, debugging, and retry purposes. The full OAuth token for every user who signs up is visible in the Inngest dashboard event log and stored in Inngest's infrastructure indefinitely.
2. Third-party breach exposure: A compromise of the Inngest account or Inngest's own infrastructure exposes the GitHub OAuth tokens of every MergeShip user, granting attackers read access to their private repositories, organisation memberships, and the ability to act as each user on GitHub.
3. Token logging: Any structured logging that captures Inngest event data (error reporting, observability tools) will capture the raw token.
4. Retry window: Inngest retries failed functions up to retries times, extending the window during which the token appears in execution context and logs.
Proposed Fix
Do not pass the OAuth token through Inngest. Instead, retrieve a GitHub App installation token on demand inside the audit function, using the installation ID (which is safe to transmit):
// bootstrapProfile -- pass installationId instead of accessToken
await inngest.send({
name: 'audit/run',
data: {
userId: profile.id,
githubHandle: profile.github_handle,
githubId,
// No accessToken here -- audit-run already supports installationId fallback
installationId: userInstallationId ?? undefined,
},
});
The audit-run function already has an installationId fallback path that mints a short-lived installation token from the GitHub App JWT. Use that path exclusively. For the bootstrap case where no installation ID is available yet, defer the audit until the installation webhook fires (which already includes the installation ID).
If a user token is strictly required for a specific API call, retrieve it from Supabase Auth within the Inngest function using the userId and the service role, rather than transmitting it through the event bus.
Severity
Critical -- every user's live GitHub OAuth token is stored in a third-party service. A single Inngest account compromise exposes all tokens. This is a systemic secrets leakage affecting 100% of users who complete signup.
I would like to work on this issue, contributing under NSoC'26. Please assign it to me.
Description
In
src/app/actions/profile.ts, thebootstrapProfileserver action sends the user's live GitHub OAuthprovider_tokenas a field in an Inngest event payload. Inngest stores event payloads in its own cloud infrastructure. This means every signing user's GitHub access token is persisted in a third-party service outside the application's control.Affected File and Lines
src/app/actions/profile.ts--bootstrapProfilefunction, Inngest send callBuggy Code
The
audit/runInngest function then reads this token:Impact
1. Token persisted in Inngest's cloud storage: Inngest retains event payloads for replay, debugging, and retry purposes. The full OAuth token for every user who signs up is visible in the Inngest dashboard event log and stored in Inngest's infrastructure indefinitely.
2. Third-party breach exposure: A compromise of the Inngest account or Inngest's own infrastructure exposes the GitHub OAuth tokens of every MergeShip user, granting attackers read access to their private repositories, organisation memberships, and the ability to act as each user on GitHub.
3. Token logging: Any structured logging that captures Inngest event data (error reporting, observability tools) will capture the raw token.
4. Retry window: Inngest retries failed functions up to
retriestimes, extending the window during which the token appears in execution context and logs.Proposed Fix
Do not pass the OAuth token through Inngest. Instead, retrieve a GitHub App installation token on demand inside the audit function, using the installation ID (which is safe to transmit):
The
audit-runfunction already has aninstallationIdfallback path that mints a short-lived installation token from the GitHub App JWT. Use that path exclusively. For the bootstrap case where no installation ID is available yet, defer the audit until theinstallationwebhook fires (which already includes the installation ID).If a user token is strictly required for a specific API call, retrieve it from Supabase Auth within the Inngest function using the
userIdand the service role, rather than transmitting it through the event bus.Severity
Critical -- every user's live GitHub OAuth token is stored in a third-party service. A single Inngest account compromise exposes all tokens. This is a systemic secrets leakage affecting 100% of users who complete signup.
I would like to work on this issue, contributing under NSoC'26. Please assign it to me.