← All articles
SECURITY Developer Security Essentials: From OWASP to Supply ... 2026-02-09 · 8 min read · security · owasp · dependency-scanning

Developer Security Essentials: From OWASP to Supply Chain Safety

Security 2026-02-09 · 8 min read security owasp dependency-scanning secrets-management sast dast supply-chain

Developer Security Essentials: From OWASP to Supply Chain Safety

Security isn't a separate discipline anymore. Every developer ships code that's exposed to the internet, handles user data, or processes payments. Waiting for a security team to review your code is a luxury most teams don't have. This guide covers the security practices every developer should build into their daily workflow.

The OWASP Top 10: What Actually Matters

The OWASP Top 10 is the industry standard list of web application security risks. Here's each one with practical context, not just definitions.

1. Broken Access Control

The most common vulnerability. Users can access resources or perform actions they shouldn't.

// VULNERABLE -- checks if user is logged in, but not if they own the resource
app.get("/api/orders/:id", requireAuth, async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  res.json(order); // Any logged-in user can see any order
});

// FIXED -- verify ownership
app.get("/api/orders/:id", requireAuth, async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  if (!order) return res.status(404).json({ error: "Not found" });
  if (order.userId !== req.user.id) return res.status(403).json({ error: "Forbidden" });
  res.json(order);
});

Prevention checklist:

2. Injection

SQL injection, NoSQL injection, OS command injection. Still prevalent despite being well-understood.

// SQL INJECTION -- never concatenate user input into queries
const query = `SELECT * FROM users WHERE email = '${req.body.email}'`;
// Input: ' OR '1'='1' --
// Result: SELECT * FROM users WHERE email = '' OR '1'='1' --'

// SAFE -- parameterized queries
const user = await db.query("SELECT * FROM users WHERE email = $1", [req.body.email]);

// SAFE -- ORM with parameterization
const user = await prisma.user.findUnique({ where: { email: req.body.email } });
// COMMAND INJECTION -- never pass user input to shell commands
const { exec } = require("child_process");
exec(`convert ${req.body.filename} output.png`); // Vulnerable
// Input: "image.png; rm -rf /"

// SAFE -- use execFile with argument arrays
const { execFile } = require("child_process");
execFile("convert", [req.body.filename, "output.png"]); // Arguments are escaped

3. Cryptographic Failures

Using weak algorithms, storing passwords in plaintext, transmitting data without TLS.

// BAD -- MD5 or SHA-256 for passwords (no salting, too fast to brute force)
import { createHash } from "crypto";
const hash = createHash("sha256").update(password).digest("hex");

// GOOD -- bcrypt or argon2 (slow by design, auto-salted)
import bcrypt from "bcrypt";
const hash = await bcrypt.hash(password, 12); // 12 rounds
const isValid = await bcrypt.compare(inputPassword, hash);

// BETTER -- argon2id (winner of Password Hashing Competition)
import argon2 from "argon2";
const hash = await argon2.hash(password, { type: argon2.argon2id });
const isValid = await argon2.verify(hash, inputPassword);

4. Security Misconfiguration

Default credentials, unnecessary features enabled, overly permissive CORS, verbose error messages in production.

// BAD -- overly permissive CORS
app.use(cors({ origin: "*" }));

// GOOD -- explicit allowed origins
app.use(
  cors({
    origin: ["https://myapp.com", "https://staging.myapp.com"],
    methods: ["GET", "POST", "PUT", "DELETE"],
    credentials: true,
  })
);

// BAD -- leaking stack traces in production
app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message, stack: err.stack });
});

// GOOD -- generic errors in production
app.use((err, req, res, next) => {
  console.error(err); // Log internally
  res.status(500).json({ error: "Internal server error" });
});

5. Server-Side Request Forgery (SSRF)

When your server makes requests to URLs provided by users, attackers can access internal services.

// VULNERABLE -- fetching user-provided URLs without validation
app.post("/api/preview", async (req, res) => {
  const response = await fetch(req.body.url); // Could be http://169.254.169.254/metadata
  const html = await response.text();
  res.json({ preview: html });
});

// SAFER -- validate and restrict URLs
import { URL } from "url";
import dns from "dns/promises";

async function isSafeUrl(urlString: string): Promise<boolean> {
  const url = new URL(urlString);
  if (!["http:", "https:"].includes(url.protocol)) return false;

  // Resolve to check for internal IPs
  const addresses = await dns.resolve(url.hostname);
  for (const addr of addresses) {
    if (
      addr.startsWith("10.") ||
      addr.startsWith("172.16.") ||
      addr.startsWith("192.168.") ||
      addr.startsWith("169.254.") ||
      addr === "127.0.0.1"
    ) {
      return false;
    }
  }
  return true;
}

Dependency Scanning: Your Biggest Attack Surface

Most applications are 90%+ third-party code. A single compromised dependency can expose every user. Supply chain attacks are increasing every year.

Scanning Tools Comparison

Tool Free? Languages CI Integration Auto-fix PRs
npm audit Yes JS/TS Built-in No
GitHub Dependabot Yes Multi-language GitHub native Yes
Snyk Free tier Multi-language All major CI Yes
Socket.dev Free tier JS/TS, Python GitHub No
Trivy Yes (OSS) Multi-language + containers All major CI No
Renovate Yes (OSS) Multi-language All major CI Yes

npm audit in CI

# .github/workflows/security.yml
name: Security Scan
on:
  push:
    branches: [main]
  pull_request:

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm audit --audit-level=high
        # Fails the build on high or critical vulnerabilities

Socket.dev: Beyond CVE Scanning

Traditional vulnerability scanners only catch known CVEs. Socket.dev analyzes package behavior -- detecting typosquatting, install scripts that exfiltrate data, and suspicious network calls.

# Install the Socket CLI
npm install -g @socketsecurity/cli

# Analyze your project
socket report create . --output report.json

# Check a specific package before installing
socket npm info lodash

Lock File Integrity

Your package-lock.json or bun.lockb is a security artifact. If an attacker modifies it to point to a malicious package version, your CI will install compromised code.

# Ensure CI uses exact versions from lock file
npm ci          # Not `npm install`, which can modify the lock file
bun install --frozen-lockfile

Secrets Management

Hardcoded secrets in source code are the most preventable security incident. Here's how to handle secrets properly.

Never Commit Secrets

# .gitignore -- bare minimum
.env
.env.*
*.pem
*.key
credentials.json
service-account.json

Pre-commit Hook for Secrets Detection

# Install gitleaks
brew install gitleaks  # macOS
# or download from https://github.com/gitleaks/gitleaks/releases

# Scan before committing
gitleaks detect --source . --verbose

# As a pre-commit hook
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

Runtime Secrets with Environment Variables

// config.ts -- validate secrets at startup, not at first use
function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }
  return value;
}

export const config = {
  database: {
    url: requireEnv("DATABASE_URL"),
  },
  stripe: {
    secretKey: requireEnv("STRIPE_SECRET_KEY"),
    webhookSecret: requireEnv("STRIPE_WEBHOOK_SECRET"),
  },
  jwt: {
    secret: requireEnv("JWT_SECRET"),
    expiresIn: "1h",
  },
} as const;

Secrets Management Services

For production systems, use a dedicated secrets manager instead of environment variables:

Service Best For Rotation Support Cost
AWS Secrets Manager AWS-hosted apps Automatic $0.40/secret/month
HashiCorp Vault Multi-cloud, on-prem Automatic Free (OSS) or paid
1Password Connect Small teams Manual Part of 1Password plan
Doppler Developer experience Automatic Free tier available
Infisical Open-source alternative Automatic Free (OSS)

SAST: Static Application Security Testing

SAST tools analyze your source code for security vulnerabilities without running it. They catch issues early -- in your editor or CI pipeline.

Tool Comparison

Tool Languages Speed False Positive Rate Free?
Semgrep 30+ languages Fast Low Free (OSS)
CodeQL 10+ languages Slow Low Free for open source
SonarQube 30+ languages Medium Medium Community edition free
Bandit Python only Fast Low Free (OSS)
Brakeman Ruby/Rails only Fast Low Free (OSS)

Semgrep in Practice

Semgrep is the standout choice for most teams. It's fast, has a massive rule library, and lets you write custom rules.

# Install
pip install semgrep
# or
brew install semgrep

# Run with default security rules
semgrep --config=auto .

# Run specific rulesets
semgrep --config=p/owasp-top-ten .
semgrep --config=p/javascript .
semgrep --config=p/typescript .
# Custom Semgrep rule -- detect hardcoded JWT secrets
# .semgrep/custom-rules.yml
rules:
  - id: hardcoded-jwt-secret
    patterns:
      - pattern: jwt.sign($PAYLOAD, "...")
    message: |
      JWT secret is hardcoded. Use an environment variable instead.
    severity: ERROR
    languages: [javascript, typescript]
    metadata:
      cwe: "CWE-798: Use of Hard-coded Credentials"
# CI integration
# .github/workflows/semgrep.yml
name: Semgrep
on: [pull_request]

jobs:
  semgrep:
    runs-on: ubuntu-latest
    container:
      image: semgrep/semgrep
    steps:
      - uses: actions/checkout@v4
      - run: semgrep ci --config=auto
        env:
          SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}

DAST: Dynamic Application Security Testing

DAST tools test your running application by sending malicious requests and analyzing responses. They find issues that static analysis can't -- like misconfigured headers or server-side vulnerabilities.

OWASP ZAP

ZAP (Zed Attack Proxy) is the most popular free DAST tool.

# Run ZAP baseline scan against your staging environment
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
  -t https://staging.example.com \
  -r report.html

# Full scan (slower, more thorough)
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-full-scan.py \
  -t https://staging.example.com \
  -r report.html
# CI integration for ZAP
# .github/workflows/dast.yml
name: DAST Scan
on:
  workflow_run:
    workflows: ["Deploy to Staging"]
    types: [completed]

jobs:
  zap-scan:
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    steps:
      - name: ZAP Baseline Scan
        uses: zaproxy/[email protected]
        with:
          target: "https://staging.example.com"
          rules_file_name: ".zap/rules.tsv"

Nuclei

Nuclei is a fast, template-based vulnerability scanner. It's particularly good for checking known CVEs and misconfigurations.

# Install
go install github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest

# Run with default templates
nuclei -u https://staging.example.com

# Run specific template categories
nuclei -u https://staging.example.com -tags cve,misconfig,exposure

Security Headers

Every web application should set these HTTP security headers:

// Express middleware for security headers
import helmet from "helmet";

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"], // Avoid if possible
        imgSrc: ["'self'", "data:", "https:"],
        connectSrc: ["'self'", "https://api.example.com"],
        fontSrc: ["'self'"],
        objectSrc: ["'none'"],
        frameSrc: ["'none'"],
        baseUri: ["'self'"],
        formAction: ["'self'"],
      },
    },
    hsts: { maxAge: 63072000, includeSubDomains: true, preload: true },
    referrerPolicy: { policy: "strict-origin-when-cross-origin" },
    noSniff: true,
    xssFilter: true,
  })
);

You can verify your headers with:

# Check security headers
curl -I https://yoursite.com

# Or use the Security Headers API
curl https://securityheaders.com/?q=yoursite.com&followRedirects=on

Building Security Into Your Workflow

Security should be automated and continuous, not a quarterly audit. Here's a practical pipeline:

The Security Pipeline

Code -> Pre-commit hooks -> CI SAST scan -> PR review -> Deploy to staging -> DAST scan -> Production
       (gitleaks)          (Semgrep)        (human)                          (ZAP/Nuclei)

Minimum Viable Security Checklist

For every project, regardless of size:

  1. Dependencies: Automated scanning with Dependabot or Renovate
  2. Secrets: Pre-commit hook with gitleaks, runtime validation of env vars
  3. Input validation: Validate and sanitize all user input at the API boundary
  4. Authentication: Use battle-tested libraries (passport, next-auth, lucia), never roll your own
  5. Authorization: Check on every request, deny by default
  6. HTTPS: Everywhere, no exceptions
  7. Security headers: Use helmet or equivalent
  8. Logging: Log authentication events, failed access attempts, and errors (but never log secrets or PII)
  9. Updates: Keep dependencies current -- most vulnerabilities are in outdated packages

What Not to Waste Time On

Summary

Security isn't about being perfect -- it's about raising the bar high enough that attackers move on to easier targets. Start with the basics: validate input, scan dependencies, manage secrets properly, and use SAST in CI. Each layer you add makes your application significantly harder to compromise. The tools are free, the integration is straightforward, and the cost of a breach makes the investment trivial by comparison.