A practical guide to understanding and fixing a common security scanner false alarm.


Contents


The Mysterious Alert

You run your security scanner on your Node.js application. Everythingโ€™s written in JavaScript and TypeScript. Your package.json has React, Next.js, Express - all the usual JavaScript suspects.

Then the security report drops:

๐Ÿ”ด CRITICAL: Golang vulnerability CVE-2024-XXXXX in esbuild
๐Ÿ”ด HIGH: Go binary exploit in @esbuild/linux-x64
โš ๏ธ MEDIUM: Multiple Golang security issues detected

Your first reaction: โ€œBut this is a Node.js project. We donโ€™t use Go!โ€

Welcome to one of the most confusing aspects of modern JavaScript development. Let me explain whatโ€™s happening and, more importantly, how to fix it.

The Plot Twist: Your JavaScript Tools Are Written in Go

Hereโ€™s the reality: some of the most popular JavaScript build tools are actually written in other languages for performance reasons:

  • esbuild - Written in Go (50-100x faster than traditional bundlers)
  • SWC - Written in Rust (20-70x faster than Babel)
  • Rome/Biome - Written in Rust
  • Turbopack - Written in Rust

When you npm install these packages, youโ€™re downloading pre-compiled binaries for your operating system. These binaries just happen to be Go (or Rust) executables.

Real-World Example: The Vanilla Extract Mystery

Let me share a common scenario Iโ€™ve seen dozens of times:

// Your package.json
{
  "dependencies": {
    "@vanilla-extract/next-plugin": "^2.4.8",
    "next": "^15.0.0",
    "react": "^18.0.0"
  }
}

Looks innocent, right? But hereโ€™s what actually gets installed:

node_modules/
โ”œโ”€โ”€ @vanilla-extract/next-plugin/
โ”‚   โ”œโ”€โ”€ @vanilla-extract/webpack-plugin/
โ”‚       โ””โ”€โ”€ @vanilla-extract/integration/
โ”‚           โ””โ”€โ”€ esbuild@0.25.5/
โ”‚               โ”œโ”€โ”€ @esbuild/darwin-arm64/  โ† Go binary for Mac ARM
โ”‚               โ”œโ”€โ”€ @esbuild/linux-x64/     โ† Go binary for Linux
โ”‚               โ”œโ”€โ”€ @esbuild/win32-x64/     โ† Go binary for Windows
โ”‚               โ””โ”€โ”€ ... (21 more platform binaries)

You now have 24 Go binaries in your node_modules.

Your security scanner finds them, checks for known CVEs, and raises the alarm.

The Critical Question: Should You Care?

The answer is nuanced and depends on when and where these binaries execute.

Understanding Build-Time vs Runtime

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Development / CI/CD (Build Time)            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ $ npm install                               โ”‚
โ”‚   โ†’ Downloads esbuild binaries              โ”‚
โ”‚                                             โ”‚
โ”‚ $ npm run build                             โ”‚
โ”‚   โ†’ esbuild EXECUTES (compiles your code)   โ”‚
โ”‚   โ†’ Generates optimized JavaScript          โ”‚
โ”‚                                             โ”‚
โ”‚ Risk: MEDIUM (if build env compromised)     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Production (Runtime)                        โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ $ npm start                                 โ”‚
โ”‚   โ†’ Node.js serves HTTP requests            โ”‚
โ”‚   โ†’ esbuild binary just sits there (idle)   โ”‚
โ”‚   โ†’ NOT executing                           โ”‚
โ”‚                                             โ”‚
โ”‚ Risk: LOW (secondary exploit only)          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Risk Matrix

Scenario Can Exploit Go Vulnerability? Risk Level
User sends HTTP request โŒ NO - Go binary not in request path ๐ŸŸข VERY LOW
Attacker gains shell access โœ… YES - Could execute binary ๐ŸŸก LOW-MEDIUM
Build environment compromise โœ… YES - Binary executes during build ๐ŸŸ  MEDIUM

Key Insight: Go binaries in production are like having a power drill in your closet. Itโ€™s there, but unless someone uses it, itโ€™s not dangerous. And if a burglar breaks into your house, the drill isnโ€™t the primary problem - the break-in is.

Solution 1: Multi-Stage Docker Builds (Best Practice)

The cleanest solution is to keep build tools out of production entirely.

Bad: Single-Stage Build

FROM node:20

WORKDIR /app
COPY package*.json ./
RUN npm ci                    # esbuild installed here

COPY . .
RUN npm run build             # esbuild executes here

EXPOSE 3000
CMD ["npm", "start"]          # esbuild STILL in container โš ๏ธ

Problem: All 24 esbuild binaries ship to production unnecessarily.

Good: Multi-Stage Build

# ===== STAGE 1: Build (Temporary) =====
FROM node:20 AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci                    # esbuild installed HERE

COPY . .
RUN npm run build             # esbuild executes HERE
# After this, we're done with esbuild โ†‘

# ===== STAGE 2: Production (What Actually Deploys) =====
FROM node:20-slim AS production

WORKDIR /app

# Install ONLY production dependencies (no esbuild!)
COPY package*.json ./
RUN npm ci --production --ignore-scripts

# Copy ONLY compiled output from builder
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public

# Run as non-root user
USER node

EXPOSE 3000
CMD ["npm", "start"]

Result:

  • Builder stage: Has esbuild, compiles your code
  • Production image: No esbuild binaries at all
  • Image size: ~60% smaller
  • Security scan: No more Go vulnerabilities

Verification

# Build production image
docker build -t myapp .

# Check if esbuild is present
docker run --rm myapp sh -c "find / -name esbuild 2>/dev/null"

# Expected output: (empty)

Solution 2: Remove Unnecessary Build Tools

Sometimes the tool causing the alert isnโ€™t even needed!

Case Study: CSS-in-JS Libraries

Many projects have this pattern:

{
  "dependencies": {
    "@vanilla-extract/next-plugin": "^2.4.8",  // โ† Brings esbuild
    "@emotion/react": "^11.0.0",
    "styled-components": "^6.0.0"
  }
}

Question: Are you actually using @vanilla-extract?

# Search your codebase
grep -r "@vanilla-extract" --include="*.tsx" --include="*.ts" src/

# If no results...
npm uninstall @vanilla-extract/next-plugin

# Problem solved!

Before Removing, Check Usage

# Find .css.ts files (vanilla-extract pattern)
find src/ -name "*.css.ts"

# Check Next.js config
grep -i "vanilla" next.config.js

# Check imports
rg "from ['\"@]vanilla-extract" src/

Solution 3: Update Dependencies

Often, newer versions have patched vulnerabilities.

# Update specific package
npm update @vanilla-extract/next-plugin

# Update all dependencies
npm update

# Check for security patches
npm audit fix

# For major version updates
npx npm-check-updates -u
npm install

Solution 4: Configure Your Security Scanner

If youโ€™ve confirmed the binaries are build-time only, configure your scanner appropriately.

GitHub Dependabot (dependabot.yml)

version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    ignore:
      # Don't alert on esbuild patch versions
      - dependency-name: "esbuild"
        update-types: ["version-update:semver-patch"]

npm Audit

# Audit only production dependencies
npm audit --production

# Generate report excluding dev dependencies
npm audit --production --json > audit-report.json

Solution 5: Accept the Risk (With Documentation)

For some projects, the Go binaries are unavoidable. Document why.

Risk Acceptance Template

## Security Finding: esbuild Go Vulnerabilities

**Package**: esbuild@0.25.5 (via @vanilla-extract/next-plugin)
**CVEs**: CVE-2024-XXXXX, CVE-2024-YYYYY
**Severity**: MEDIUM

**Risk Assessment**:
- Vulnerability exists in build-time tooling only
- Not present in production container (multi-stage build)
- Not accessible to end users
- Build environment secured with:
  - Ephemeral build agents
  - Network segmentation
  - Access controls
  - Build process monitoring

**Mitigation Strategies**:
1. Multi-stage Docker builds (binaries not in production)
2. Build environment hardening (non-root, read-only FS)
3. Regular dependency updates (monthly)
4. Continuous monitoring for new CVEs

**Business Justification**:
- @vanilla-extract required by component library
- No viable alternative for our use case
- Performance benefits outweigh minimal risk

**Risk Level**: LOW (secondary exploit vector only)

**Accepted By**: Security Team
**Date**: 2025-10-01
**Next Review**: 2026-03-01

Real-World Attack Scenarios

Letโ€™s be concrete about what could actually happen.

Scenario 1: Direct Web Exploitation

Attacker โ†’ HTTP Request โ†’ Your App
                            โ†“
                         Node.js handles request
                         esbuild NOT involved

Result: esbuild vulnerability IRRELEVANT

Risk: None. The Go binary doesnโ€™t execute.

Scenario 2: Container Compromise โ†’ Binary Execution

Step 1: Attacker exploits DIFFERENT vulnerability (e.g., RCE in express)
Step 2: Gains shell access to container
Step 3: Discovers esbuild binary
Step 4: Attempts to exploit esbuild CVE for privilege escalation

Result: Could work, but you're already compromised

Risk: Low-Medium. The real problem was Step 1.

Scenario 3: CI/CD Pipeline Compromise

Step 1: Attacker compromises build server
Step 2: npm install runs โ†’ esbuild post-install scripts execute
Step 3: Exploits esbuild vulnerability DURING BUILD
Step 4: Injects malicious code into build artifacts

Result: Supply chain attack

Risk: Medium. This is the most realistic threat.

Mitigation:

  • Isolated, ephemeral build agents
  • Signed commits required
  • Build artifact signing and verification
  • Network egress monitoring during builds

Best Practices Checklist

For All Projects

  • Use multi-stage Docker builds
  • Run production containers as non-root user
  • Implement read-only root filesystem where possible
  • Regular dependency updates (automated, but reviewed by engineers)
  • Security scanning in CI/CD pipeline
  • Document accepted risks

For Build Environment

  • Ephemeral build agents (destroyed after each build)
  • Network segmentation for build infrastructure
  • Artifact signing and verification
  • Build process monitoring and alerting
  • Access controls on build systems

For Dependency Management

  • Lock file committed to version control
  • Use npm ci not npm install in production
  • Regular security audits (npm audit)
  • Dependency update automation (Dependabot, Renovate, etc.)
  • Review new dependencies before adding

The Bottom Line

Finding Go vulnerabilities in your Node.js project is surprisingly common and usually low risk if handled correctly.

Quick Decision Tree

Go vulnerabilities detected
         โ†“
Is the package needed?
         โ”œโ”€ NO โ†’ Remove it
         โ””โ”€ YES
              โ†“
         Is it in production image?
              โ”œโ”€ NO โ†’ Multi-stage build
              โ””โ”€ YES
                   โ†“
              Can you use multi-stage builds?
                   โ”œโ”€ YES โ†’ Implement them
                   โ””โ”€ NO โ†’ Document risk + harden container

Priority Order

  1. Remove if possible (fastest, zero risk)
  2. Multi-stage builds (best practice)
  3. Update dependencies (ongoing maintenance)
  4. Configure scanners (reduce noise)
  5. Document accepted risk (compliance)

Tools and Resources

Dependency Analysis

# Why is this package installed?
npm ls esbuild

# Full dependency tree
npm ls --all > dependency-tree.txt

# Find all Go binaries
find node_modules -name "*.go" -o -name "*darwin*" -o -name "*linux*" | grep -i esbuild

# Check production vs dev dependencies
npm ls --production
npm ls --dev

Security Scanning

Learning Resources

Conclusion

Go vulnerabilities in Node.js projects are a symptom of the modern JavaScript ecosystemโ€™s reliance on high-performance native tooling. While the initial security alert can be alarming, the actual risk is often much lower than it appears.

The key is understanding:

  1. When these binaries execute (build-time vs runtime)
  2. Where theyโ€™re deployed (development, CI/CD, production)
  3. How to properly isolate them (multi-stage builds, containerization)

By following the solutions outlined in this post, you can either eliminate these vulnerabilities entirely (best outcome) or properly contextualize them with appropriate mitigations (acceptable outcome).


Remember: presence doesnโ€™t equal execution. Just because a binary is in your node_modules doesnโ€™t mean it poses a direct threat. Focus your security efforts on the vulnerabilities that are actually in your runtime attack surface.


Feel free to contact me for any suggestions and feedbacks. I would really appreciate those.

Thank you for reading!

Back to Topโฎญ