Back to Checks
vercel

Vercel API tokens are not stale or unused for over 90 days

authentication_no_stale_tokens
Service: authenticationSeverity: medium
OfficialBuilt-inVerified
Categories:trust-boundariesvercel-hobby-plan
Resource Type:NotDefined

Vercel API tokens are assessed for staleness by checking whether each token has been active within the last 90 days. Stale tokens that remain unused for extended periods represent unnecessary access credentials that increase the attack surface. Tokens with no recorded activity are also flagged.

Risk

Stale tokens that have not been used for over 90 days may belong to decommissioned integrations, former team members, or forgotten automation. These tokens remain valid and could be compromised or misused without detection, as their inactivity makes suspicious usage harder to notice in access logs.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler vercel --checks authentication_no_stale_tokens

Recommendation

Regularly audit API tokens and revoke any that have not been used within 90 days. Implement a token lifecycle management process that includes periodic reviews, automatic expiration dates, and documentation of each token's purpose and owner.

Remediation

Other

1Sign in to the Vercel dashboard
2Navigate to Account Settings > Tokens
3Review the last active date for each token
4Revoke or delete tokens that have not been used in over 90 days
5Contact token owners to confirm whether the token is still needed
6Implement a regular token review process (e.g., quarterly)

Check Code

from datetime import datetime, timedelta, timezone
from typing import List

from prowler.lib.check.models import Check, CheckReportVercel
from prowler.providers.vercel.services.authentication.authentication_client import (
    authentication_client,
)


class authentication_no_stale_tokens(Check):
    """Check if API tokens have been used recently.

    This class verifies whether each Vercel API token has been active within
    the configured threshold (default: 90 days). Stale tokens that remain
    unused pose a security risk as they may have been forgotten or belong
    to former team members.
    """

    def execute(self) -> List[CheckReportVercel]:
        """Execute the Vercel Stale Token check.

        Iterates over all tokens and checks if each token has been active
        within the configured threshold. The threshold is configurable via
        ``stale_token_threshold_days`` in audit_config (default: 90 days).

        Returns:
            List[CheckReportVercel]: A list of reports for each token.
        """
        findings = []
        now = datetime.now(timezone.utc)
        stale_threshold_days = authentication_client.audit_config.get(
            "stale_token_threshold_days", 90
        )
        stale_cutoff = now - timedelta(days=stale_threshold_days)

        for token in authentication_client.tokens.values():
            report = CheckReportVercel(
                metadata=self.metadata(),
                resource=token,
                resource_name=token.name,
                resource_id=token.id,
            )

            if token.active_at is None:
                report.status = "FAIL"
                report.status_extended = (
                    f"Token '{token.name}' ({token.id}) has no recorded activity "
                    f"and is considered stale."
                )
            elif token.active_at < stale_cutoff:
                days_inactive = (now - token.active_at).days
                report.status = "FAIL"
                report.status_extended = (
                    f"Token '{token.name}' ({token.id}) has not been used for "
                    f"{days_inactive} days (last active: "
                    f"{token.active_at.strftime('%Y-%m-%d %H:%M UTC')}). "
                    f"Threshold is {stale_threshold_days} days."
                )
            else:
                report.status = "PASS"
                report.status_extended = (
                    f"Token '{token.name}' ({token.id}) was last active on "
                    f"{token.active_at.strftime('%Y-%m-%d %H:%M UTC')} "
                    f"(within the last {stale_threshold_days} days)."
                )

            findings.append(report)

        return findings