Vercel hands attackers your build pipeline
Technical IR playbook for a Vercel CI/CD compromise: attack chain, MITRE ATT&CK mapping, telemetry gaps, containment sequence, and residual exposure.
A Vercel project compromise is a CI/CD compromise with a public DNS record attached. The blast radius is not the marketing page that got defaced. It is every environment variable the project holds, every preview URL the team has accepted as trusted, every downstream API that trusts the deployed function’s identity, and every browser that fetched edge-cached JavaScript between intrusion and eviction. Treating it as a web defacement misreads the surface. The exposure is the build pipeline, the deployment plane, and the persistent trust the Vercel tenant holds across GitHub, cloud accounts, third-party APIs, and customer sessions.
Start from the fact that Vercel is not a static host. It is a remote build executor with production write access. Every push to a connected branch triggers a containerised build inside Vercel’s infrastructure. That build runs npm install against the project’s package.json, executes the framework’s build command - next build, astro build, vite build - and writes output that is immediately promoted to the edge. The build container has outbound internet reach, writes to ephemeral storage, and exposes every environment variable scoped to the build step as process.env. Any code that executes inside that container at build time runs with that reach. A postinstall hook in a newly added transitive dependency has the same capability as a deliberately written build script committed by a senior engineer. The trust assumption is that the code in the repository and the dependency graph it resolves to is what the maintainer intended. When that assumption breaks, the consequences reach production in the time it takes to resolve a lockfile.
The trust boundaries inside a Vercel tenant are fewer than most teams assume. The Git integration holds an OAuth grant against the source forge - typically GitHub App installation with permissions to read repository contents and write commit statuses, sometimes deployment keys, and often an install-level webhook. The deployment plane holds per-project environment variables, split across development, preview, and production scopes. Each scope is exposed to any build that targets it. Preview scope is exposed to any pull request build, including PRs from forks if the team has not explicitly disabled that setting. The tenant holds personal access tokens for the Vercel API - long-lived bearer credentials that can create deployments, read logs, modify environment variables, and add team members. The edge runtime executes the built artifacts on Cloudflare-class infrastructure. Edge functions have their own execution context - restricted APIs, no filesystem, limited Node globals - but they have full outbound HTTP and full access to the environment variables the deployment was built with. Everything downstream of the Vercel identity - Supabase, Neon, Upstash, Clerk, Stripe, the team’s own backend - trusts the deployed function because the deployed function holds the credential.
Initial access to a Vercel tenant lands through one of five doors. Stolen Vercel access tokens, typically exfiltrated from a developer workstation via infostealer malware. StealC, Lumma, RedLine, Atomic Stealer - all of them enumerate ~/.vercel, ~/.npmrc, ~/.aws, ~/.config/gh, and browser credential stores. A Vercel access token in a stealer log is a direct production write primitive. GitHub account compromise - session cookie theft, OAuth phishing, or a legitimate device session replayed from a stealer log - grants push access to every connected repository. A push to the default branch triggers a production build. A merged PR to any branch with a deploy hook triggers a production build. OAuth app compromise - a malicious app granted by a team member that holds repo or workflow scope - gives silent push capability without touching the audited session path. Compromised maintainer on an upstream package that the project depends on transitively, where the attacker publishes a version that satisfies the existing semver range and ships a postinstall that activates only when specific environment variables are present. Dependency confusion, where an internal scoped package name is claimed on the public npm registry and the build resolver pulls the public version because the private registry was misconfigured or unreachable during the build.
The recent pattern - observable across the Shai-Hulud npm worm, the Ledger ConnectKit compromise, the ua-parser-js incident, the multiple @solana/web3.js attacks, and the wave of infostealer-to-deploy-token pivots - is that CI/CD compromise does not require a vulnerability in the platform. It requires a credential or a package with inherited privilege. MITRE T1195.002, compromise software supply chain. MITRE T1199, trusted relationship. MITRE T1078.004, valid accounts in cloud services. No exploit is fired. No memory is corrupted. The platform behaves exactly as designed. The design trusts the push, the lockfile, the token.
Once the attacker has write to the repository or write to the Vercel tenant, build-time exploitation is where the interesting work happens. The objective is not the Vercel tenant itself. It is everything the tenant holds credentials for, plus the browsers that will fetch the deployed JavaScript. A postinstall hook fires during npm install. It runs with the identity of the build container. process.env at that moment contains every production environment variable the project has scoped to the build step. The attacker’s code iterates the environment, filters for values matching entropy thresholds and known credential patterns - JWTs, AWS access key IDs starting with AKIA or ASIA, Stripe keys starting with sk_live_, Supabase service role JWTs, database URLs with embedded passwords - and exfiltrates over HTTPS to attacker-controlled infrastructure. The exfiltration channel almost always looks like a legitimate telemetry endpoint. A Cloudflare Workers URL. A Vercel-hosted attacker domain. A Firebase Realtime Database write. A Discord webhook. A Telegram bot API call. None of these trigger egress controls on Vercel’s build infrastructure because Vercel’s build infrastructure does not enforce egress controls on customer builds. That is not a Vercel failure. It is the product. The product runs arbitrary code at build time.
Build-time exploitation does not stop at credential theft. The more durable objective is artifact tampering. The attacker modifies the build output before it is written to the deployment. The surface is broad. In a Next.js project, the attacker can inject into pages/_app.tsx or app/layout.tsx equivalents at build time by patching the file on disk after dependency resolution and before the framework’s compile step runs. They can hook into the bundler via a malicious Webpack plugin or Vite plugin that the compromised dependency registered. They can modify the emitted .next/server/app/*.js files directly after the build completes but before Vercel uploads the output. They can inject into the edge function bundle that gets deployed to the edge runtime. The delivered payload is often a loader - a few hundred bytes of JavaScript that conditionally fetches the real payload from an attacker-controlled domain based on the visitor’s IP, geography, user-agent, or referrer. This is the magecart model applied to supply-chain-compromised deployments. The loader is small enough to miss in a diff review if a diff review happens. The real payload is delivered at runtime only to targets, making static analysis of the deployment bundle insufficient.
What the payload does in the browser is a function of the attacker’s objective. For financial targets, it hooks form submissions and exfiltrates card data before the legitimate payment SDK sees it. For crypto-adjacent targets - the pattern in the Ledger ConnectKit compromise - it replaces wallet interaction libraries with wrappers that swap transaction recipients. For credential harvesting, it injects into authentication flows and captures credentials before they are hashed, or captures session tokens after authentication completes. For access, it installs a service worker that remains registered on the victim’s browser across sessions, intercepting requests even after the malicious deployment has been rolled back. MITRE T1059.007, JavaScript execution. MITRE T1557, adversary-in-the-middle. MITRE T1056.003, web portal credential capture.
Persistence inside the Vercel tenant is where incident responders consistently underinvest. The attacker who reaches the Vercel API through a stolen token does not need to keep using that token. The token can be rotated, revoked, or burned. What cannot be easily undone is the attacker’s side-effects on the tenant configuration. New deploy hooks created against arbitrary branches - attacker-controlled URLs that trigger a production deploy when called. New team members added with admin roles, especially when added through the GitHub SSO path where the audit trail points at the invited identity rather than the inviter. New environment variables injected into the project with names that blend into the existing set - NEXT_PUBLIC_ANALYTICS_ID, SENTRY_AUTH_TOKEN, CF_API_TOKEN - that actually hold attacker C2 endpoints or credentials used by the injected payload. Modifications to vercel.json that add rewrites, redirects, or middleware paths pointing at attacker-controlled origins, making the production domain a traffic interposer. Changes to the project’s connected Git repository - adding the attacker’s key as a deploy key, adding webhooks, modifying branch protection - that survive any Vercel-side credential rotation. MITRE T1098.001, account manipulation via additional cloud credentials. MITRE T1136.003, create cloud account. MITRE T1556.007, hybrid identity modification.
Lateral movement from Vercel reaches whatever the environment variables grant access to. Database URLs with embedded credentials - Neon, Supabase, Planetscale, Upstash - unlock direct read and write against production data stores. Service role JWTs on Supabase bypass row-level security entirely, giving the attacker admin-scope access to every row. AWS access key IDs and secrets - commonly scoped to S3 for asset uploads or SES for transactional email, but frequently over-permissioned - open the connected AWS account. SendGrid, Resend, Postmark API keys permit outbound email from the team’s domains, which is rarely logged in security tooling and which attackers use for targeted phishing against the team’s customers. Stripe restricted keys permit refund creation, payout modification, and customer data reads. OAuth app secrets for the team’s own integrations allow session forgery against users who authenticated through those apps. The Vercel tenant is a concentrated credential store. Compromise of the tenant is compromise of everything downstream of the tenant that did not implement token binding, hardware-backed attestation, or short-TTL rotation.
Telemetry is where the defensive gap is widest and where the incident response posture has to be rebuilt from scratch for most teams. The core observation is that the attack executes outside the organisation’s EDR perimeter. Vercel’s build containers run on Vercel infrastructure. The organisation has no process creation events, no network connection logs, no file write telemetry, no DNS query records from the environment in which the postinstall hook executed. Sysmon does not cover it. CrowdStrike does not cover it. SentinelOne does not cover it. The build is a black box that produces an artifact and a deployment log. The deployment log is textual - stdout and stderr captured by Vercel - and it contains only what the build process chose to emit. Any attacker writing a postinstall hook for this environment writes it to be silent. The build completes green. The deployment ships. The incident began and ended before any detection primitive the organisation controls had a chance to fire.
What does fire, and where, matters to the IR sequence. The Vercel audit log records deployment events, environment variable changes, team membership changes, token creation and revocation, and deploy hook creation. These entries are shallow - they record the actor identity and the event type but not the content of changes. An attacker who creates a new environment variable is logged as having done so; the value of the variable is not in the audit event. The audit log is available through the Vercel API and the dashboard, and it should be the first artefact pulled in the IR timeline. Filter the log window around the suspected initial access time. Correlate every token creation, every team member change, every environment variable modification against the known-expected activity of the tenant. Anything unattributable is an artefact of the intrusion.
GitHub audit logs cover the Git side of the compromise. On organisation-owned repositories, audit events for push activity, OAuth app installations, personal access token creation, and SSH key additions are retained and queryable through the Enterprise audit log API. Session events - the IP, user-agent, and geolocation of authenticated sessions - are the highest-signal artefact when the question is whether the legitimate account was compromised or whether a deploy key was abused. A push from an IP outside the normal geographic footprint of the developer, or a push from a user-agent that does not match the developer’s usual client, is the kind of signal that separates account takeover from malicious maintainer. On personal repositories the audit retention is shorter and thinner. Escalate immediately to organisation-scoped equivalents if the project is not already there.
On the cloud side, the downstream telemetry is where containment depth is measured. AWS CloudTrail records every API call made with the credentials that were exposed. Filter CloudTrail by the access key ID that was in the compromised environment variable set. Every event returned is an action the attacker took or that the legitimate application took - the separation is by source IP, user-agent, and timing. CloudTrail records GetCallerIdentity calls, which attackers almost always fire early to confirm the credentials and enumerate the identity’s permissions. ListBuckets, ListUsers, ListRoles, ListPolicies in rapid succession from a single IP within seconds of each other is enumeration behaviour that does not occur in normal application flow. CreateAccessKey, AttachUserPolicy, CreateUser, AssumeRole to an unexpected role, or any IAM modification, is persistence establishment. MITRE T1098.001 again, now visible in the downstream telemetry the attacker could not suppress.
Identity providers - Okta, Entra ID, Google Workspace - are the second telemetry surface that captures the compromise even when the build container does not. If the Vercel tenant is SSO-federated, authentication events against the IdP are retained. Look for session creation from non-standard IPs, ATO-class signals - impossible travel, new device registration, MFA prompt bypass via session replay. The MFA bypass patterns that matter here are session cookie replay from infostealer logs and OAuth token reuse - neither generates an MFA event because the attacker is not authenticating, they are replaying an already-authenticated session.
Network telemetry from the edge is thinner than teams expect but not zero. If the deployment was serving malicious JavaScript to real users, request logs at the edge show the outbound traffic pattern - the attacker’s payload fetching additional stages, exfiltrating captured form data, or beaconing to C2. Vercel’s Analytics and log drains capture this when configured. Most teams do not configure log drains in advance. The lesson is procedural - log drains to a durable sink are a baseline deployment requirement. The forensics question during IR is whether any user-facing malicious payload was served, for how long, and to which sessions. The answer shapes customer notification obligations under GDPR Article 33, the Australian Privacy Act Notifiable Data Breaches scheme, and US state-level breach notification laws. An unlogged edge is an unbounded notification scope.
Browser-side, the injected payload leaves artefacts on victims’ machines that are outside the organisation’s visibility but inside the threat intel community’s. Service workers registered by malicious deployments persist until unregistered. Malicious domains fetched by the payload surface in Cloudflare Gateway logs, DNS logging tools, and Passive DNS databases. CDN providers and browser vendors that run client-side telemetry - Google Safe Browsing, Microsoft SmartScreen - begin flagging the compromised deployment’s URLs once enough victims’ browsers report suspicious activity. The organisation learning about the compromise from a customer support ticket referencing a browser security warning is a recurring pattern. It is also a terminal signal that the compromise has been live long enough for external systems to have observed it.
Mapping the intrusion to MITRE ATT&CK produces a chain that is instructive because nothing in it requires novel tradecraft. Initial access lands through T1078.004, valid accounts, or T1195.002, compromised software supply chain. Execution is T1059.007, JavaScript at build or run time, sometimes T1059.004 if the postinstall spawns shell. Persistence is T1098.001, additional cloud credentials via new Vercel tokens or team members; T1546 variants if the persistence is in repository hooks or CI configuration. Defense evasion is T1550, use of alternate authentication material - the stolen session or token - and T1036.005, masquerading via legitimate deployment tooling. Credential access is T1552.001, credentials in environment variables, extracted during build. Discovery is T1526, cloud service discovery, against the downstream services the credentials unlock. Lateral movement is T1078.004 again as the attacker pivots into AWS, Supabase, Stripe. Collection is T1530, data from cloud storage; T1119, automated collection in the case of bulk database scrapes. Command and control is T1071.001, web protocols, blending with legitimate HTTPS egress. Exfiltration is T1567.002, exfiltration to web service - Discord, Telegram, Pastebin, GitHub Gists, attacker-controlled Vercel deployments, or Cloudflare Workers endpoints. Impact varies - T1565 data manipulation for payment redirection, T1499 for availability, T1657 for financial theft. Every technique in this chain is documented. None of it is zero-day. The compromise works because the controls that would break the chain - signed build artifacts, workload identity instead of long-lived tokens, egress allowlisting at the build container, dependency pinning with integrity verification, runtime attestation of deployed code - are not defaults on the platform and are rarely configured on top.
The IR sequence, in order, is revoke, rotate, review, rebuild, notify, audit. Revoke every Vercel access token on the tenant. Every personal token, every team token, every integration token. Revoke the GitHub App installation or regenerate its credentials. Disable and regenerate every deploy hook URL. Invalidate every OAuth app client secret that the tenant created. The first action is to make the attacker’s existing access primitives stop working. Expect the attacker to attempt re-entry through secondary persistence - additional team members, alternate tokens, deploy hooks - so the second sweep is necessary.
Rotate every environment variable, on every scope, for every project. Assume exposure. Production, preview, and development scopes were all readable by any build that ran during the compromise window. Every downstream credential those variables held must be rotated upstream - new AWS access keys, new database passwords, new Stripe keys with the old ones revoked, new Supabase service role JWTs with the old ones revoked through a project-level JWT secret rotation that also invalidates user sessions. Where rotation is not possible - hardcoded secrets in third-party systems that do not support key rotation without downtime - the secret must be treated as permanently exposed and the upstream service treated as compromised until proven otherwise.
Review every deployment produced during the compromise window. Pull the build logs, diff the deployed bundle against a known-good build from before the intrusion, and inspect for injected code. Focus on the entry points: _app, _document, root layout, client components that render on every page, service worker registrations, middleware, edge functions. A diff tool that operates on the minified bundle output is more useful than a source diff because the injection may bypass source and insert at the bundler plugin layer. Tools like sourcemap-decoder, js-beautify, and static analyzers that flag suspicious patterns - fetch to unexpected domains, eval, Function constructor, dynamic script injection, credential exfiltration patterns - narrow the review. Every deployment from the compromise window is suspect. Rolling back to the last known-good commit is not sufficient if the compromise lives in a dependency that is still in the lockfile.
Rebuild the lockfile from a known-good state. Audit package-lock.json or pnpm-lock.yaml against the dependency graph that existed before the compromise window. Pin every direct dependency to an exact version. Scan the full transitive tree against known supply chain compromise indicators - Socket.dev, Snyk, npm audit signatures for the recent incident families. Regenerate the lockfile in a clean environment, not on the developer workstation that may itself be compromised, and commit the regenerated lockfile separately so the change is reviewable. Where a dependency was confirmed compromised, do not simply upgrade to the fixed version - audit the project’s own code for calls into that dependency during the compromise window, because the malicious version may have exfiltrated data that passed through its API surface.
Notify affected parties. The notification scope is defined by the data the compromised deployment could reach. If customer sessions were active during the serving window and the deployed bundle contained credential capture logic, every authenticated session during that window is a potential exposure. If the compromised build had database credentials and CloudTrail or database audit logs show reads of customer PII during the window, the breach notification obligation is concrete. Timeline is regulated. GDPR is 72 hours from awareness. The Australian Notifiable Data Breaches scheme is 30 days to assess and notify. US state law varies. Legal and privacy counsel drives the notification, not engineering.
Audit the scope of trust the Vercel tenant held and narrow it permanently. Every credential that was in an environment variable should have been a short-lived workload identity assumed at runtime through an identity federation mechanism - AWS IAM Roles Anywhere, GCP Workload Identity Federation, Azure Federated Credentials, OIDC token exchange against the Vercel-provided OIDC issuer. The federated model means a leaked long-lived secret does not exist because no long-lived secret is stored. The build and the edge function receive a short-TTL credential bound to the deployment context, attested by a verifiable JWT the downstream service validates. This is the control that breaks the chain at the credential-access technique. Every team operating critical production workloads on Vercel should have this configured. Most do not. The compromise is the forcing function.
Residual exposure after eviction is the part of the playbook that most IR engagements underrepresent. A rotated token does not retroactively protect the data the attacker already exfiltrated. The stolen database contents, customer records, internal source code, and secrets that passed through the environment are in the attacker’s possession permanently. Any credentials that were long-lived and are still valid in other systems the organisation did not rotate - because the IR team did not know they existed in that environment variable set - remain live. The service workers registered on customer browsers by the malicious deployment persist until the browser unregisters them, which requires either the browser navigating to the origin and receiving an updated service worker that unregisters the malicious one, or the user manually clearing site data. Preview deployments that were created during the compromise window may still be publicly accessible through their deployment URLs, and those preview URLs may have been indexed by search engines or cached by third-party services. The Vercel tenant’s Git integration, if compromised at the GitHub App level, may have written to repositories other than the directly targeted one. A full organisation-wide audit of the GitHub App’s recent activity is mandatory.
The technical reality post-patch is that the Vercel platform itself is not the vulnerability. The vulnerability is the design of CI/CD as a system that executes attacker-reachable code with production-reach credentials. Vercel is a clean implementation of that design. GitHub Actions is another. CircleCI, Jenkins, GitLab CI - every build-and-deploy platform that runs third-party code against secrets in process environment has the same failure mode. The mitigations that matter are structural. Short-lived credentials via OIDC federation. Signed build artifacts with in-toto or SLSA attestation verified at deploy gate. Egress allowlisting at the build container - a capability Vercel does not expose as of this writing. Dependency integrity verification via npm ci against a pinned lockfile with registry signature verification. Separation of build identity from deploy identity - the build container should not hold production credentials, only the deploy step should, and the deploy step should execute fixed, audited logic that the attacker cannot modify by changing the repository. Detection of supply chain compromise through behavioural analysis of postinstall scripts - a capability platforms like Socket, Snyk, and Phylum provide at the package level. Restricted workflows for production deployment - require an approval gate, a human verification, or a delay that allows detection between build and deploy.
None of these controls prevent the initial access vector. A stolen token is still a stolen token. A compromised maintainer is still a compromised maintainer. What the controls do is reduce the blast radius. A token that only creates deployments but cannot read environment variables is less valuable. A build that cannot reach the public internet cannot exfiltrate. A deploy that cannot ship without a signed attestation cannot ship a tampered artifact. A production credential that is issued at runtime with a five-minute TTL and is bound to the deployment’s immutable hash cannot be reused after rotation. The compromise still happens. The recovery is bounded. The notification scope is narrower. The residual exposure is measurable.
The operational posture that follows from this analysis is that every team running production on a managed CI/CD platform has to treat that platform as part of the incident surface, not as a trusted black box. The platform’s audit logs are a first-class security data source and must be shipped to the organisation’s SIEM. The platform’s access tokens are privileged credentials and must be treated like AWS root keys - inventoried, rotated on a schedule, and revoked aggressively. The platform’s build environment is an execution surface that arbitrary code can reach, and every dependency change is a candidate for supply chain review. The edge is the perimeter, and the build is the door. The door does not lock itself.
The compromise replays the same structural failure every quarter. Someone’s token leaks. Someone’s package ships a malicious postinstall. Someone’s OAuth app grants more than it needed. The platform behaves as designed. The downstream systems trust what the platform deploys. Production serves the payload. The telemetry to detect it was not wired up. The IR team learns the tenant configuration for the first time at 2AM on a Thursday. The playbook above is what the team should have run before the page was rendered. It is what remains useful after.
See also: NordVPN for tunneled traffic when operating outside controlled networks.
#ad Contains an affiliate link.
Keep Reading
data exposureSanctioned keylogger, unlocked back end
Meta's exposed employee keystroke telemetry is not an AI story. It is a third-party data-handling failure: T1056.001 collection, an unauthenticated store, T1530 read.
vulnerability researchSixty-three days to patch a forked parser
Technical breakdown of the FrontierOS RCE: a forked XML parser, an unpatched two-year-old CVE, and the fork-tracking failure that shipped it.
msspYour MSSP is selling you blindness.
MSSPs run perimeter-era detection while attackers operate inside the identity boundary. The gap is structural, not a resourcing problem.
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.