vercelsior project now scans .github/workflows/*.yml for cache-poisoning
risk (CWE-494), since Vercel deploy previews are commonly built in CI:
- prj-gha-cache-poison (WARN, MEDIUM) — an
actions/cachestep whosekeyandrestore-keyscarry no integrity binding (neither ahashFiles()over a lockfile nor a commit-SHA context such asgithub.sha). An entry poisoned from a lower-trust context (fork PR, feature branch) persists into later builds, including deploy previews. - prj-gha-cache-restore-keys (WARN, LOW) — a bound
keypaired with an unboundrestore-keysprefix, which reopens the fallback path on a key miss.
The parser tolerates quoted uses: values, skips commented-out steps, caps
per-file scan size, and surfaces an ERROR (never a false all-clear) when a
cache step cannot be fully parsed.
No new dependencies: workflows are parsed with a stdlib indentation-aware scanner, preserving the zero-dependency single-binary build. No breaking changes; existing CheckIDs and invocations are unchanged.
False-positive reduction release. No breaking changes: existing CheckIDs and the
scan/probe/project invocations are unchanged; this run tightens heuristics, adds
the route-007 and fw-003-unconfirmed findings, and recalibrates several
severities.
Tightened heuristic detections that fired on benign-but-matching input. Name
matching now uses word/segment boundaries instead of unanchored substrings
(shared helpers in internal/checks/matching.go):
- sec-001 no longer flags public locators like
NEXTAUTH_URLas plain-text secrets; value-shape and an allowlist distinguish locators from credentials. - sto-001/003 (takeover) match the real
vercel-dns.comCNAME target and honor wildcard/apex domain coverage, so legitimately-served subdomains and unrelated hosts (e.g.vercelstatus.com) are no longer flagged. - infra-040/041, stor-002, flag-001 match whole name segments, so
monkey/oauth-routes/NEXT_PUBLIC_API_URLno longer match. - prev-003 stops flagging bare shell metacharacters in build commands;
it flags
eval/sh -candcurl | shinstead. - route-004 is now scoped to cloud-metadata destinations (SSRF to IMDS,
including AWS IMDSv6
fd00:ec2::254) on rewrites/proxies only; private and loopback destinations move to the new low-severity route-007. - Severity recalibration: iam-010 (SAML, an Enterprise feature) and
dom-001 (unverified domain) downgraded to Low; flag-002 scoped to
security-sensitive flags and downgraded to Medium; fw-003 aggregates
per-category findings and reports unconfirmed (absent) categories as
low-severity fw-003-unconfirmed; iam-014 and dep-001 only fire on
a confirmed state (explicit policy
off; a project with a connected Git repo).
- dom-024 now detects DMARC records published under the absolute name
(
_dmarc.example.com) or byv=DMARC1value, not only the relative_dmarcname, so a present DMARC record is no longer reported as missing. - stor-001/002 again detect the
POSTGRES_URL_NON_POOLINGstorage binding (and nowPOSTGRES_PRISMA_URL/KV_REST_API_READ_ONLY_TOKEN).
The trifecta release. Vercelsior becomes a three-mode Vercel security tool —
scan (account CSPM via the API), probe (black-box DAST against a
deployed URL), and project (local IaC/SAST, pre-deploy) — plus SARIF 2.1.0
output, a full release pipeline (goreleaser, multi-arch Docker, SBOMs), and
GitHub Code Scanning support. Fully backward compatible: the legacy
vercelsior [flags] invocation still runs scan unchanged.
- Subcommand architecture. The CLI now routes
scan,project,probe,version, andhelpsubcommands (zero new dependencies).scanis the existing account/team API audit (CSPM). Fully backward compatible: invokingvercelsiorwith no subcommand or a leading flag (e.g.vercelsior --token ...,VERCEL_TOKEN=... vercelsior) still runsscanexactly as before.run/routeare unit-tested for the legacy and explicit forms. - Probe mode (
vercelsior probe <url>) — black-box DAST. Tests a deployed URL from the outside with no token. Checks: CVE-2025-29927 Next.js middleware auth bypass (compares a blocked baseline against thex-middleware-subrequestbypass header and only reports when protection is observably skipped — scoped to Next.js targets to avoid false claims), exposed JavaScript source maps, security headers (CSP/HSTS/ X-Frame-Options/X-Content-Type-Options/Referrer-Policy/Permissions-Policy), and technology disclosure (X-Powered-By). Newinternal/proberpackage (16 unit tests via an injectableFetcher, no network in tests) emits the standardmodels.ScanResult, so all reporters (incl. SARIF) work unchanged. All egress reuses the SSRF-guarded, redirect-pinned probe client via a newclient.Probemethod, with same-host enforcement on discovered URLs. - Project mode (
vercelsior project [path]) — local IaC/SAST. Static analysis of a project repo with no token, for pre-deploy / shift-left CI. Checks: Next.js CVE matrix against thenextversion in package.json, committed/client-exposed secrets in.env*(hardcoded values,NEXT_PUBLIC_/VITE_/etc. leakage,.gitignorecoverage), risky next.config.* flags (productionBrowserSourceMaps,ignoreBuildErrors,ignoreDuringBuilds,poweredByHeader), and vercel.json issues (external redirects/rewrites, missing security headers). Newinternal/projectpackage reads throughio/fs(tested withfstest.MapFS, no real FS) and a sharedinternal/nextjspackage holds the version/CVE logic. Conservative secret detection (strong token shapes + sensitive-name-plus-real-value + entropy) to minimize false positives. - The trifecta is complete: Vercelsior now tests Vercel security from the
inside (
scan), the repo (project), and the outside (probe). - SARIF 2.1.0 output (
-f sarif). Newinternal/reporter/sarif.goemits a GitHub Code Scanning-compatible SARIF log. Findings (FAIL/WARN/ERROR; PASS excluded) become results with severity→level mapping, per-findingsecurity-severityderived from the 1–10 risk score, deterministic rule ordering, and stablepartialFingerprints(SHA-256 of check + resource) so a persisting finding is tracked across scans rather than re-reported as new. Non-source cloud findings get a syntheticvercel/<category>/<resource>artifact URI plus a logical location naming the resource. - Release automation.
.goreleaser.yml+.github/workflows/release.ymlproduce cross-platform binaries (linux/darwin/windows × amd64/arm64), archives,checksums.txt, CycloneDX SBOMs, a multi-arch GHCR Docker image (ghcr.io/su1ph3r/vercelsior), and Homebrew/Scoop manifests on tagged releases. - Docker images.
Dockerfile(build-from-source) andDockerfile.goreleaser(release image), Alpine-based withca-certificates. - golangci-lint in CI. New
lintjob +.golangci.yml(govet, staticcheck, errcheck, ineffassign, unused, gofmt, goimports, misspell, unconvert, bodyclose). Tests now run with-race. - Build-version stamping.
main.versionis now injected via-ldflags -X main.version=...;make build, Docker, and goreleaser all stamp the version, and it is recorded in SARIF output.
- Single Next.js CVE source of truth. The CVE matrix and version parser
previously existed in two copies (the scan path and project mode). Both now
consume
internal/nextjs(Match/ParseVersion), so a CVE is added once. Behavior preserved; the matrix is covered by an integrity guard plus per-CVE matrix tests (95% statement coverage).
takeoverfalse-positive CRITICALs. A swallowed per-projectListProjectDomainserror dropped that project's domains from the "claimed" set, turning its own CNAMEs/aliases into bogus dangling-takeover findings (sto-001CRITICAL /sto-002HIGH).collectProjectDomainsnow propagates the error;Runemits a distinctsto-001-errorand skips the negative checks rather than asserting against an incomplete set.- Case-insensitive
--checks/--skip-checks. CheckID filtering matched case-sensitively while IDs are emitted lowercase, so the documented uppercase form (--skip-checks IAM-001) silently no-opped (and an uppercase allowlist dropped every finding).canonicalCheckIDnow folds case before the alias lookup. - Probe checks no longer assert safety when they cannot run. The CVE-2025-29927 and source-map probes returned PASS ("not exploitable" / "clean") when every probe/fetch errored (e.g. a WAF tarpitting the bypass header); they now return ERROR when nothing could be evaluated.
- Project mode distinguishes unreadable from absent files. A permission/IO
error on
package.json/.env*/next.config.*/vercel.jsonis now an ERROR finding instead of being treated as "file absent" (silent skip). iam-005no longer fails open on an object-shapedscopespayload (extracted as the testedisFullAccessScopeshelper).- SPF detection (
dom-023) no longer false-positives on a TXT value of exactlyv=spf1. chk-002reports the number of deployments actually inspected, not the full recent-deployment count.- Terminal summary now prints the
Errorscount when non-zero, so a coverage gap (checks that could not run) is visible rather than only inflating the posture score. - Probe PoC evidence truncation is UTF-8-safe (
internal/prober), matching the client's existing rune-boundary handling. - SSRF guard hardened beyond the v0.1.4 work: blocks CGNAT
100.64.0.0/10,0.0.0.0/8, protocol/benchmark ranges, and the IPv4 embedded in NAT64 (64:ff9b::/96) / 6to4 (2002::/16) addresses, while still allowing public IPv4 wrapped in those transitions. - Packaging: anchored the
.gitignorebinary patterns (/vercelsior) so they no longer match thecmd/vercelsior/source directory — without this a fresh clone / CI / release build would omit the package's source files.
- Dead code: unused
evidence()helper ininternal/checks/base.goand unusedfindFinding()test helper.
Hardening release: 33 issues addressed across security, correctness, resource management, and reporting. No user-facing breaking changes — legacy CheckIDs still match via a compatibility alias map.
- SSRF — DNS rebinding TOCTOU closed in
--liveheader probe.ProbeHeaderspreviously performed its allowlist check and the dial as two independent DNS lookups. A hostile authoritative DNS could return a public IP on the first resolution and127.0.0.1/169.254.169.254/ an RFC 1918 address on the second, pointing the probe at internal endpoints. The dialer now resolves once vianet.DefaultResolver.LookupIPAddr, validates every returned IP, and dials the pinned IP directly.CheckRedirectreturnshttp.ErrUseLastResponseso 3xx responses cannot re-enter the resolver for a different host. TLS SNI and theHostheader continue to use the URL's original hostname so certificate validation works normally. (CWE-918, CWE-367) - Recorder no longer writes secrets to disk.
--record DIRresponse bodies are walked and values whose keys matchvalue,decryptedValue,token,bearerToken,accessToken,refreshToken,secret,signingSecret,privateKey,password,leakedUrl, orinviteCodeare replaced with[REDACTED]. Response headersSet-Cookie,X-Vercel-Id,X-Vercel-Trace, andX-Amz-Request-Idare also scrubbed. Contributors who record fixtures against a live account no longer leak plain-text env var values, token metadata, or session cookies when committing the fixtures. Redaction is case-insensitive and preserves JSONnullliterals. (CWE-200) - Replay path-traversal defense-in-depth.
safeRecordingPathrejects keys containing/,\, or.., and confirms the resolved absolute path is inside the recording directory. (CWE-22) - Pooled probe client.
ProbeHeaderspreviously built a freshhttp.Transportper call; N projects produced N independent connection pools and background goroutines held ~90s by keep-alive. A single client is now constructed lazily viasync.Onceand reused across every probe.
stor-001false-negative fixed.hasTrustedIPspreviously defaulted totruefor any non-niltrustedIpsobject, suppressing the finding when Vercel returned an empty object or an object without anaddressesarray. It now defaults tofalseand flips totrueonly when a non-emptyaddressesarray is present.- Scanner suppression order. Suppression is now applied before the min-severity filter, so a suppressed check below the threshold appears as
[SUPPRESSED] Passinstead of being silently dropped. - Silent non-403 API errors eliminated. Eight check modules previously only handled
IsPermissionDenied(err)and swallowed every other error (5xx, rate-limit, DNS, timeout), producing silent audit gaps. A sharedapiErrorFindinghelper now emits an INFO-severity ERROR finding for each non-403 failure infirewall.go,routes.go,preview.go,verification.go,iam.go,git.go,logging.go,rolling.go,sandbox.go,tls.go,storage.go, andheaders.go. - JSON list-response parsing. Nine list endpoints (
ListLogDrains,ListEdgeConfigs,ListEdgeConfigTokens,ListSecureComputeNetworks,ListSharedEnvVars,ListCertificates,ListBulkRedirects,ListProjectRoutes,ListSandboxes, plusListWebhooks) now shareparseListResponse, which surfaces real API errors instead of silently falling back when the expected shape is missing. getPaginatedMapsreports malformed items. Items that fail to unmarshal are counted and logged once per endpoint instead of vanishing silently.- Header-probe gaps are visible. A new
hdr-000(Info/Error) finding records every domain whose probe failed (SSRF guard, DNS, TLS, timeout). Previously the domain silently disappeared from the audit. pickProductionDomainpropagates API errors so the caller distinguishes "project has no domains" from "domain listing failed" and emits the appropriate finding.iam-015CheckID collision resolved. The team-members permission finding previously reusediam-015(already claimed by "IP Addresses Visible in Observability"), which collided in the scanner'sCheckID|ResourceIDdedup. It now emitsiam-019.dom-011no longer flagsredirectStatusCode == 0(missing field) as a non-permanent redirect.takeover.gonon-permission error usessto-001-errorto avoid colliding with the primarysto-001findings under the sameN/AResourceID.parseVersionrejects malformed patches (e.g.15.2.x,1.2.3.4) instead of returningpatch=0, which could cause false-positive CVE matches.njs-002emits an Error finding when no deployment exposes a framework version, instead of silently implying the project is safe.GetPaginatedno longer mutates the caller's params map. Pagination cursors (limit,until) are written to an internal copy.- UTF-8 safe truncation. Three truncation sites (
reporter/markdown.go:97,client/client.go:124error body,client/client.go:502evidence log) now preserve valid UTF-8 on rune boundaries; previously they could split a multi-byte character and emit replacement characters. - Scan IDs use nanosecond precision (
UnixNano) so two scans started in the same second don't overwrite each other's reports. os.MkdirAllerror is now fatal instead of silently producing confusing secondary errors when the output directory is unwritable.- Diff report write errors reported.
WriteDiffMarkdown/WriteDiffJSONerrors are now logged instead of discarded. - Canceled CheckID typos harmonized.
inf-030→infra-040,rol-001→roll-001,sbx-001→sand-001. User configs that reference the legacy forms continue to match via a compatibility alias map ininternal/config/config.go— no action required for existing.vercelsiorfiles.
saveResponsecloses the original body on read errors. Previously a read failure inio.ReadAlldropped theResponseBodyreference without closing it, leaking the underlying TCP connection until keep-alive timeout.
hdr-000— Header Probe Failed (Info, Error). Surfaces audit gaps caused by probe failures during--livemode. Does not affect exit code.
- 9 new test files across
internal/checks,internal/client,internal/config,internal/models,internal/reporter, andinternal/scanner, plus an extension tointernal/client/client_test.go - 367 test cases total, 0 failures
- Coverage:
models97.6%,config94.4%,client46.6%,reporter37.4%,scanner35.4% - Every behavioral fix has a targeted regression test
scanner.Runfilter logic extracted toapplyFiltersanddedupeFindingsfor testability (behavior unchanged)- Shared helpers added:
utf8SafeTruncate(inreporter,checks, andclient),apiErrorFinding(inchecks),parseListResponse(inclient),safeRecordingPath(inclient),isDisallowedProbeIP(inclient)
- Module path in
go.modand all import statements corrected fromgithub.com/su1ph/vercelsiortogithub.com/Su1ph3r/vercelsior. Previous versions could not be installed viago installbecause the module path did not resolve to the real repository.go install github.com/Su1ph3r/vercelsior/cmd/vercelsior@latestnow works correctly.
- fw-010 — WAF Rules in Detect-Only Mode: flags OWASP or managed rules set to detect/monitor-only
- log-024 — Webhook Missing Signing Secret: flags webhook endpoints without signing secrets
- sec-026 — Client-Exposed Sensitive Environment Variable: detects secrets leaked via framework prefixes (VITE_, GATSBY_, REACT_APP_, NUXT_PUBLIC_)
- route-004 — Rewrite Destination Points to Internal Target: SSRF detection for rewrites/redirects to internal IPs, localhost, or cloud metadata endpoints
- sto-003 — Dangling A Record Pointing to Vercel: detects A records to Vercel IPs without a matching project domain
- Added CVE-2025-55182 (React Server Components deserialization RCE, CVSS 10.0)
- Added CVE-2025-49826 (ISR cache poisoning, CVSS 7.5)
- Added CVE-2025-59471 (Image optimization memory exhaustion, CVSS 5.9)
- Added CVE-2025-59472 (PPR resume endpoint DoS, CVSS 5.9)
- Fixed CVE-2025-29927 false positives for pre-middleware Next.js versions (<12)
- HTTP 403 responses now produce explicit per-check permission-denied findings instead of silently skipping checks
- Each check module reports which API endpoints require additional token scopes
- Deduplicated findings for per-project loops (one permission finding per API endpoint, not per project)
- Fixed firewall check false positive: 403 on GetFirewallConfig no longer reports "Firewall Not Configured"
- Fixed SSRF detection bypass via URL userinfo (
@) syntax in route-004 - Fixed mass false-positive Critical findings when ListProjects fails in subdomain takeover checks
- Fixed OIDC check (dep-009) inverted condition that flagged disabled OIDC as a pass
- Fixed silent failures in secrets, webhook, and deployment visibility checks when API calls error
- Fixed
parseVersionambiguity for version "0.x.x" (now uses explicit ok flag) - Fixed remediation KB
{project_id}placeholder substitution for env-var-scoped findings - Added 429 rate-limit responses to evidence log
- Tightened recording file permissions from 0644/0755 to 0600/0700
- Added SSRF protection to
ProbeHeadersvia DNS resolution checks blocking private/reserved IPs - Route SSRF detection uses
net/url.Parsefor correct RFC 3986 handling including IPv6
Initial release — Vercel security auditing tool with 130+ checks across 20 categories.