A practical guide to understanding and fixing a common security scanner false alarm.
Contents
- The Mysterious Alert
- The Plot Twist: Your JavaScript Tools Are Written in Go
- Real-World Example: The Vanilla Extract Mystery
- The Critical Question: Should You Care?
- Solution 1: Multi-Stage Docker Builds (Best Practice)
- Solution 2: Remove Unnecessary Build Tools
- Solution 3: Update Dependencies
- Solution 4: Configure Your Security Scanner
- Solution 5: Accept the Risk (With Documentation)
- Real-World Attack Scenarios
- Best Practices Checklist
- The Bottom Line
- Tools and Resources
- Conclusion
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
notnpm 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
- Remove if possible (fastest, zero risk)
- Multi-stage builds (best practice)
- Update dependencies (ongoing maintenance)
- Configure scanners (reduce noise)
- 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
- npm audit - Built-in npm security audit
- Socket.dev - Supply chain security
- Trivy - Container 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:
- When these binaries execute (build-time vs runtime)
- Where theyโre deployed (development, CI/CD, production)
- 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!