DEV Community

Cover image for OAuth 2.0 Scope Creep: the Attack Vector the Vercel Incident Exposed and How to Audit It in Your Integrations
Juan Torchia
Juan Torchia Subscriber

Posted on • Originally published at juanchi.dev

OAuth 2.0 Scope Creep: the Attack Vector the Vercel Incident Exposed and How to Audit It in Your Integrations

OAuth 2.0 Scope Creep: the Attack Vector the Vercel Incident Exposed and How to Audit It in Your Integrations

Most developers review OAuth scopes exactly once: the day they set up the integration. After that, the connection "works" and nobody looks at it again. Yeah, you read that right. And that's not individual carelessness — it's the industry's default pattern. It's also exactly the vector that cases like the Vercel/GitHub OAuth incident put on full display.

My thesis is direct: the Vercel incident wasn't a technical vulnerability in the classical sense. It was a least-privilege failure applied to OAuth. The security of third-party integrations is only as good as the scope you granted them — and most developers don't revisit that once the integration is running. That gap between "it works" and "it's well-designed" is the problem I want to tear apart here.


What OAuth 2.0 Scope Creep Is and Why It Matters Right Now

Scope creep in OAuth isn't a CVE. It won't show up in a vulnerability scanner. It's a gradual process: a third-party integration starts by requesting the bare minimum, and over time — through convenience, documentation copy-paste, or sheer lack of review — it ends up accumulating permissions that go well beyond its original function.

RFC 6819 — OAuth 2.0 Threat Model and Security Considerations documents this explicitly as an attack surface threat: if a token with excessive scopes gets compromised, the blast radius is proportional to what that token can do, not what it should do. The RFC calls this out in section 4.1.2 as scope elevation — one of the attack vectors that authorization servers and clients must actively mitigate.

In the Vercel/GitHub case, the public discussion centered on what access the authorized GitHub app had to act on behalf of the user — and whether that access was proportional to its declared function. I'm not going to reconstruct the full incident here (the OAuth Supply-Chain Risk video I published covers that hook). What I care about is the class of problem it represents: a third party holding more permissions than it needs, with nobody auditing them periodically.

The uncomfortable part: most of the CI/CD, hosting, and observability systems you use today have OAuth access to repositories, container registries, or deployment APIs. Do you know exactly what scopes they have authorized? When did you last check?


RFC 6819: What It Says and What It Doesn't

RFC 6819 is the technical foundation for understanding the OAuth 2.0 threat model. Some concrete points relevant to this analysis:

What it does say:

  • Clients must request the minimum scope necessary for their function (least-privilege principle, section 3.1).
  • Authorization servers must implement mechanisms so users can review and revoke active tokens.
  • The scope of a compromised token directly determines the blast radius of an attack.
  • Accumulating scopes over time (scope creep) increases the attack surface without increasing declared functionality.

What it doesn't say — and this matters:

  • The RFC doesn't specify how to implement scope auditing in production. That lives at the application layer.
  • It doesn't define token rotation frequency or automatic revocation policies. That depends on the authorization server and each organization's policies.
  • It doesn't solve the problem of long-lived refresh tokens — which in many integrations are functionally equivalent to permanent credentials.

The RFC is the map of the territory. The "how to actually audit it" part is the responsibility of the team that designed the integration.


Where People Get It Wrong: the Common Recipe and Its Hidden Cost

The pattern I keep seeing repeated in OAuth integrations has three moments:

1. The initial setup with generous scopes

When you integrate a third-party service — a deployment platform, an analytics system, a CI tool — the official documentation almost always shows the broadest example. repo instead of repo:read. admin:org instead of read:org. It's easier to get it working. Copy-paste wins.

The problem is that scope gets baked into the token and the authorization. And nobody trims it afterward.

2. The integration "works" and disappears from the radar

Once the pipeline is green, the integration becomes invisible infrastructure. Nobody touches it because nobody wants to break it. That's completely logical from an operational standpoint — and it's exactly the behavior that creates accumulated scope creep.

There's a direct parallel with observability endpoint configuration: if you never review what you're exposing, you end up with more attack surface than you think you have. Same principle I applied when analyzing what to expose and what to hide in Spring Boot Actuator.

3. Revocation as reaction, not process

In most teams, OAuth tokens get revoked when there's an incident or when someone leaves the team. There's no proactive review process. That means integrations that no longer exist can still have active tokens with broad scopes — sitting there waiting to be used by whoever found the credentials.

The hidden cost: if that token leaks — through a log leak, a public repo that included environment variables, a compromised dependency — the attacker gets access proportional to the broadest scope the token allows, not the minimum necessary.


How to Audit OAuth Scopes in Existing Integrations: Actionable Checklist

This is the part most posts skip. Understanding the problem isn't enough — you need a process to review it.

Step 1: Inventory All Active OAuth Integrations

For each integration, answer:

  • What scopes does it currently have authorized?
  • When was it last used?
  • Is it still necessary?

On GitHub, you can review authorized apps at Settings > Applications > Authorized OAuth Apps. On Google, at myaccount.google.com/permissions. Most identity providers have a similar screen.

Step 2: Evaluate Each Scope Against Its Real Function

For each scope, the question is simple but uncomfortable: does the integration need this permission to do what it says it does?

Use this criteria:

// Scope evaluation criteria
type ScopeAuditResult = {
  scope: string;          // scope name
  function: string;       // what the integration uses it for
  necessary: boolean;     // does removing it break functionality?
  alternative: string;    // more restrictive scope if one exists
  action: "keep" | "reduce" | "revoke";
};

// Example: CI/CD integration
const auditCICD: ScopeAuditResult[] = [
  {
    scope: "repo",
    function: "read code for builds",
    necessary: false, // only needs to read, not write
    alternative: "repo:read or contents:read",
    action: "reduce",
  },
  {
    scope: "admin:repo_hook",
    function: "create webhooks for build triggers",
    necessary: true,
    alternative: "write:repo_hook (more specific)",
    action: "reduce",
  },
  {
    scope: "delete_repo",
    function: "none declared",
    necessary: false,
    alternative: "not needed",
    action: "revoke",
  },
];
Enter fullscreen mode Exit fullscreen mode

Step 3: Review Long-Lived Refresh Tokens

An active refresh token without expiration is functionally equivalent to a permanent credential. Ask:

  • Does this provider's authorization server support refresh token rotation?
  • Do the tokens have expiration configured?
  • Is there any periodic rotation process?

If the provider doesn't support automatic rotation, the alternative is establishing a manual process with a defined frequency — quarterly is reasonable for production integrations.

Step 4: Implement Anomalous Usage Detection

An authorized scope that never gets used is a direct candidate for revocation. If the authorization server exposes usage logs per scope (some do), review them. If not, implement your own logging at the integration layer:

// Logging middleware for OAuth integrations in Next.js
// Records which scopes are actually used in production
import { NextRequest, NextResponse } from "next/server";

export async function middleware(req: NextRequest) {
  const authHeader = req.headers.get("authorization");

  if (authHeader?.startsWith("Bearer ")) {
    // Log the endpoint that required the token
    // to map real usage vs authorized scopes
    console.log(
      JSON.stringify({
        timestamp: new Date().toISOString(),
        path: req.nextUrl.pathname,
        method: req.method,
        // Don't log the full token — only the jti if available
        tokenPresent: true,
      })
    );
  }

  return NextResponse.next();
}

export const config = {
  // Apply only to routes that consume external OAuth APIs
  matcher: ["/api/integrations/:path*"],
};
Enter fullscreen mode Exit fullscreen mode

Step 5: Define a Periodic Review Process

Without a process, the audit is a one-time event. Scope creep is a continuous process. You need them to meet:

  • Minimum suggested frequency: every 90 days for active integrations, every 30 days for integrations with broad scopes.
  • Mandatory trigger: any team change (developer onboarding or offboarding) must trigger a review of active tokens.
  • Defined owner: someone on the team has to be responsible for the OAuth integration inventory. No owner, no process.

Architectural Controls: Prevent Before You Audit

Reactive auditing is necessary. But there are controls you can build into the design from day one:

1. Internal Authorization Proxy

Instead of each service directly handling third-party OAuth tokens, you can centralize in an internal proxy that acts as an authorization intermediary. The proxy validates scopes, logs usage, and can revoke without changing the downstream integration. It's more infrastructure, but the centralized control is worth the complexity in systems with many integrations.

2. Context-Bound Token Binding

If the provider's authorization server supports it, bind tokens to specific contexts (IP range, user agent, resource). This doesn't eliminate scope creep but reduces the blast radius of a compromised token.

3. Granular Scopes From Day One

The cheapest decision is the first one: request the most restrictive scope the provider offers during initial setup. If you need more permissions later, you ask for them. The cost of requesting less and adjusting is far lower than the cost of auditing and revoking after the fact.

This connects to the minimum surface principle I apply at other layers — from OpenTelemetry traces that cross the edge runtime to the controls you evaluate before exposing observability endpoints. The pattern is consistent: less exposed surface, smaller possible blast radius.

4. Alert on Unused Scopes

If you can instrument scope usage (see step 4 of the checklist), configure an alert when an authorized scope shows no usage in 30 days. That's a direct candidate for review and possible revocation.


The Limits of This Analysis: What You Can't Conclude Without More Data

It would be dishonest of me to present this as a complete guide without marking its limits:

  • I don't have access to the internal details of the Vercel incident. The analysis is based on public discussion and the RFC 6819 threat model. If the root cause was different, the diagnosis changes.
  • RFC 6819 documents threats but doesn't prescribe implementations. What counts as "minimum necessary scope" depends on the specific context of each integration — there's no universal number.
  • The audit frequency I'm suggesting (90 days) has no backing in formal research. It's a reasonable craft judgment, not a statistically validated metric. Adjust it based on the risk level of each integration.
  • Long-lived refresh tokens are a real risk, but the concrete impact depends on the provider. Some authorization servers implement automatic rotation and reuse detection; others don't. Check the specific provider's documentation before assuming the worst case.

FAQ: OAuth Scope Creep and Integration Auditing

What's the difference between scope creep and privilege escalation in OAuth?
Privilege escalation is an active attack where someone tries to obtain permissions they don't have. Scope creep is a passive process: the permissions were already legitimately granted, but they've accumulated beyond what's necessary. RFC 6819 treats them as distinct threats — the first in section 4.1.3, the second as a consequence of violating the least-privilege principle.

How do you know what scopes an integration actually needs?
The most practical approach: start with the most restrictive scope the provider's documentation offers, try to get the integration working with that, and expand only when you hit a concrete authorization error. Most scope creep problems come from the inverse approach: start with everything and never review.

Are OAuth tokens with refresh tokens riskier than direct access tokens?
In terms of risk duration, yes. An access token that expires in 1 hour limits the damage to that window. A refresh token without expiration (or with expiration measured in months) acts as a semi-permanent credential. RFC 6819 in section 4.1.2 explicitly recommends implementing refresh token rotation and suspicious reuse detection.

Is it worth implementing an authorization proxy for third-party integrations?
Depends on the number of integrations and risk level. For a personal project with two OAuth integrations, no. For a system with dozens of active integrations handling sensitive data, the centralized proxy is a reasonable control. The implementation cost is real — don't underestimate it.

What happens if you revoke a token from an active integration?
The integration stops working until the user re-authorizes it. In CI/CD or deployment integrations, that can block the pipeline. That's why revocation needs to come with a planned re-authorization process, not as an emergency reaction.

Does scope creep also apply to internal integrations (between your own services)?
Absolutely. If you use OAuth between your own microservices — what's known as machine-to-machine with client credentials flow — the same principle applies. Each service should have exactly the scope needed for its function. A notification service doesn't need write scope over user data. This vector is less visible than third-party integrations but equally relevant.


Closing: the Permission Debt Nobody Measures

There's a type of technical debt that never shows up in any backlog: permission debt. Integrations configured with broad scopes because it was faster, tokens that were never reviewed because they "work," active refresh tokens from services that no longer exist.

The Vercel incident is useful not because it's unique — it's useful because it's public and documented. The same pattern exists in almost any system with more than five active OAuth integrations. The difference between the one that had the incident and the one that hasn't yet is, in many cases, just a matter of time and which integration got compromised first.

What I think is honest to tell you: there's no tool that solves this for you. RFC 6819 gives you the threat model. The checklist above gives you the process. But the decision of when to audit, what to revoke, and how to design the minimum scope for each integration is technical judgment — the kind you build through craft and through the discomfort of reviewing things that already work.

My practical recommendation: right now, open the authorized apps screen on your GitHub, your Google Workspace, or your main identity provider. Count how many integrations have scopes you don't recognize as necessary. If that number makes you uncomfortable, you already have your first step.


Original source:


This article was originally published on juanchi.dev

Top comments (0)