Security Supply Chain Dependencies DevSecOps

Securing Your Extension Against Supply Chain Attacks

E
Extendable Team
· 12 min read

Browser extensions are high-value targets for supply chain attacks. A compromised extension gains access to browsing data, stored credentials, and user activity across all websites. This guide covers defensive measures to protect your extension and users from supply chain threats.

Understanding Supply Chain Risks

Extension supply chain attacks can occur through:

  • Compromised dependencies: Malicious code in npm packages
  • Hijacked developer accounts: Attacker pushes malicious update
  • Build system compromise: Malware injected during CI/CD
  • Extension acquisition: Legitimate extension sold to malicious actor
High-Profile Incidents:
  • The Great Suspender: Acquired, turned malicious
  • Nano Defender: Developer sold extension to malware operator
  • event-stream: npm package backdoor affected many extensions
  • ua-parser-js: Supply chain attack via npm

Dependency Security

Auditing Dependencies

# Run security audit
npm audit

# Check for known vulnerabilities
npx audit-ci --moderate

# Generate SBOM (Software Bill of Materials)
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
// package.json - Lock dependencies to exact versions
{
  "dependencies": {
    "lodash": "4.17.21",  // Exact version, not ^4.17.21
    "axios": "1.6.2"
  },
  "overrides": {
    // Force specific versions of transitive dependencies
    "minimist": "1.2.8"
  }
}

Dependency Lockfiles

Always commit lockfiles and verify integrity:

# Verify lockfile integrity
npm ci  # Fails if lockfile doesn't match package.json

# Check for lockfile tampering in CI
npm ci --ignore-scripts  # Don't run postinstall scripts

Minimizing Dependencies

// Before: Heavy dependency for simple task
import _ from 'lodash';
const result = _.get(obj, 'a.b.c');

// After: Native implementation
const result = obj?.a?.b?.c;

// Before: Moment.js (500KB+)
import moment from 'moment';
const formatted = moment().format('YYYY-MM-DD');

// After: Native Intl API
const formatted = new Date().toISOString().split('T')[0];

Dependency reduction checklist:

  • Can this be done with native browser APIs?
  • Do I need the entire library or just one function?
  • Is this dependency actively maintained?
  • Does this dependency have its own dependencies?

Vendoring Critical Dependencies

For security-critical code, consider vendoring:

// vendor/simple-hash.js - Vendored and audited
export function simpleHash(str) {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash |= 0;
  }
  return hash.toString(16);
}

// main.js
import { simpleHash } from './vendor/simple-hash.js';

Build Pipeline Security

Secure CI/CD Configuration

# .github/workflows/build.yml
name: Secure Build

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read  # Minimal permissions

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false  # Don't persist git credentials

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci --ignore-scripts  # Skip postinstall scripts

      - name: Run security audit
        run: npm audit --audit-level=high

      - name: Run linting
        run: npm run lint

      - name: Run tests
        run: npm test

      - name: Build extension
        run: npm run build

      - name: Verify build integrity
        run: |
          # Check no unexpected files in build
          find dist -type f | sort > actual-files.txt
          diff expected-files.txt actual-files.txt

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: extension-build
          path: dist/
          retention-days: 30

Build Reproducibility

// build.config.js
module.exports = {
  // Pin tool versions
  nodeVersion: '20.10.0',
  npmVersion: '10.2.3',

  // Deterministic builds
  sourceMap: false,  // Don't include in production
  minify: true,
  deterministic: true,

  // Verify build output
  expectedFiles: [
    'manifest.json',
    'background.js',
    'popup.html',
    'popup.js',
    'content.js',
    'icons/icon16.png',
    'icons/icon48.png',
    'icons/icon128.png'
  ],

  // No eval or dynamic imports
  noEval: true,
  noDynamicImport: true
};

Signing and Verification

// sign-build.js
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

function hashDirectory(dir) {
  const hashes = [];

  function walk(directory) {
    const files = fs.readdirSync(directory).sort();

    for (const file of files) {
      const fullPath = path.join(directory, file);
      const stat = fs.statSync(fullPath);

      if (stat.isDirectory()) {
        walk(fullPath);
      } else {
        const content = fs.readFileSync(fullPath);
        const hash = crypto.createHash('sha256').update(content).digest('hex');
        const relativePath = path.relative(dir, fullPath);
        hashes.push(`${hash}  ${relativePath}`);
      }
    }
  }

  walk(dir);
  return hashes.join('\n');
}

// Generate manifest of build
const manifest = hashDirectory('./dist');
fs.writeFileSync('./dist/CHECKSUMS.sha256', manifest);

console.log('Build checksums:');
console.log(manifest);

Account Security

Protecting Developer Accounts

Chrome Web Store:

  • Enable 2-factor authentication
  • Use hardware security keys
  • Limit team member access
  • Review audit logs regularly

Code Repository:

  • Require signed commits
  • Protect main branch
  • Require code reviews
  • Use branch protection rules
# Configure signed commits
git config --global commit.gpgsign true
git config --global user.signingkey YOUR_GPG_KEY_ID

Access Control

# .github/CODEOWNERS
# Require approval for sensitive files
/manifest.json @security-team
/build/ @security-team
/.github/workflows/ @security-team
/src/background.js @security-team
Account Security Checklist:
  • Enable 2FA on all accounts (store, git, npm)
  • Use unique passwords per service
  • Rotate credentials periodically
  • Audit access permissions quarterly
  • Set up alerts for suspicious activity

Runtime Security

Content Security Policy

// manifest.json
{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline'"
  }
}

Input Validation

// Validate all external data
function validateApiResponse(response) {
  // Schema validation
  const schema = {
    type: 'object',
    required: ['data', 'version'],
    properties: {
      data: { type: 'array' },
      version: { type: 'string', pattern: '^\\d+\\.\\d+\\.\\d+$' }
    }
  };

  if (!validateSchema(response, schema)) {
    throw new Error('Invalid API response format');
  }

  // Sanitize string content
  if (response.data) {
    response.data = response.data.map(item => ({
      ...item,
      text: sanitizeHtml(item.text),
      url: validateUrl(item.url) ? item.url : null
    }));
  }

  return response;
}

function sanitizeHtml(html) {
  const div = document.createElement('div');
  div.textContent = html;  // Escapes HTML
  return div.innerHTML;
}

function validateUrl(url) {
  try {
    const parsed = new URL(url);
    return ['http:', 'https:'].includes(parsed.protocol);
  } catch {
    return false;
  }
}

Subresource Integrity

For any external resources (if absolutely necessary):

<!-- If loading external scripts (avoid if possible) -->
<script
  src="https://cdn.example.com/lib.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
></script>

Monitoring and Detection

Extension Integrity Monitoring

// background.js - Self-integrity check
async function checkExtensionIntegrity() {
  // Get expected hashes from secure storage
  const { expectedHashes } = await chrome.storage.local.get('expectedHashes');
  if (!expectedHashes) return; // First run

  // Check critical files
  const filesToCheck = [
    'background.js',
    'content.js',
    'popup.js'
  ];

  for (const file of filesToCheck) {
    const response = await fetch(chrome.runtime.getURL(file));
    const content = await response.text();
    const hash = await hashString(content);

    if (hash !== expectedHashes[file]) {
      // Integrity violation detected
      console.error(`Integrity check failed for ${file}`);
      await reportIntegrityViolation(file, hash, expectedHashes[file]);

      // Optionally disable extension
      // chrome.management.setEnabled(chrome.runtime.id, false);
    }
  }
}

async function hashString(str) {
  const encoder = new TextEncoder();
  const data = encoder.encode(str);
  const hashBuffer = await crypto.subtle.digest('SHA-256', data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

// Check on startup and periodically
chrome.runtime.onStartup.addListener(checkExtensionIntegrity);
chrome.alarms.create('integrity-check', { periodInMinutes: 60 });

Anomaly Detection

// Monitor for suspicious behavior patterns
class BehaviorMonitor {
  constructor() {
    this.metrics = {
      apiCalls: 0,
      storageWrites: 0,
      tabAccess: 0,
      networkRequests: 0
    };
    this.baseline = null;
    this.windowStart = Date.now();
  }

  track(metric) {
    this.metrics[metric]++;
  }

  checkAnomaly() {
    const windowMinutes = (Date.now() - this.windowStart) / 60000;

    if (!this.baseline) {
      // Establish baseline after 24 hours
      if (windowMinutes >= 1440) {
        this.baseline = { ...this.metrics };
        this.resetWindow();
      }
      return;
    }

    // Check for significant deviations
    for (const [metric, value] of Object.entries(this.metrics)) {
      const expected = this.baseline[metric] * (windowMinutes / 1440);
      const deviation = value / (expected || 1);

      if (deviation > 10) {  // 10x normal activity
        this.reportAnomaly(metric, value, expected);
      }
    }
  }

  async reportAnomaly(metric, actual, expected) {
    console.warn(`Anomaly detected: ${metric} is ${actual} (expected ~${expected.toFixed(0)})`);

    // Send to monitoring service
    await fetch('https://api.yourextension.com/security/anomaly', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        extensionVersion: chrome.runtime.getManifest().version,
        metric,
        actual,
        expected,
        timestamp: Date.now()
      })
    });
  }

  resetWindow() {
    this.metrics = { apiCalls: 0, storageWrites: 0, tabAccess: 0, networkRequests: 0 };
    this.windowStart = Date.now();
  }
}

Incident Response

Preparation

## Security Incident Response Plan

### Detection
- [ ] Monitor store reviews for security complaints
- [ ] Set up alerts for anomalous API usage
- [ ] Watch for reports on social media / forums

### Containment
1. Identify affected versions
2. Disable auto-update if possible
3. Publish clean update immediately
4. Notify affected users

### Recovery
1. Identify attack vector
2. Remove compromised code/dependencies
3. Reset all credentials
4. Rebuild from known-good source

### Post-Incident
1. Document incident timeline
2. Update security practices
3. Notify users of resolution
4. Consider bug bounty program

Summary

Supply chain security requires defense in depth: minimize dependencies, verify builds, protect accounts, and monitor runtime behavior. The effort invested in security protects both your users and your reputation.

Key security measures:

  • Audit and minimize dependencies
  • Lock exact dependency versions
  • Secure CI/CD pipeline
  • Enable 2FA on all accounts
  • Implement runtime integrity checks
  • Monitor for anomalous behavior
  • Have an incident response plan