Skip to main content

Command Palette

Search for a command to run...

Developer Security Essentials: From OWASP to Supply Chain Safety

Published
10 min read
D
Practical guides for developers: TypeScript, developer tools, CI/CD, and modern web development. We cover the tools that make devs more productive.

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.

OWASP web application security project logo

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:

  • Deny by default -- require explicit grants, not explicit denials
  • Check authorization on every request, not just at login
  • Use row-level security in your database when possible
  • Never rely on client-side access control (hiding UI elements isn't security)

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

ToolFree?LanguagesCI IntegrationAuto-fix PRs
npm auditYesJS/TSBuilt-inNo
GitHub DependabotYesMulti-languageGitHub nativeYes
SnykFree tierMulti-languageAll major CIYes
Socket.devFree tierJS/TS, PythonGitHubNo
TrivyYes (OSS)Multi-language + containersAll major CINo
RenovateYes (OSS)Multi-languageAll major CIYes

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:

ServiceBest ForRotation SupportCost
AWS Secrets ManagerAWS-hosted appsAutomatic$0.40/secret/month
HashiCorp VaultMulti-cloud, on-premAutomaticFree (OSS) or paid
1Password ConnectSmall teamsManualPart of 1Password plan
DopplerDeveloper experienceAutomaticFree tier available
InfisicalOpen-source alternativeAutomaticFree (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

ToolLanguagesSpeedFalse Positive RateFree?
Semgrep30+ languagesFastLowFree (OSS)
CodeQL10+ languagesSlowLowFree for open source
SonarQube30+ languagesMediumMediumCommunity edition free
BanditPython onlyFastLowFree (OSS)
BrakemanRuby/Rails onlyFastLowFree (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/action-baseline@v0.10.0
        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

  • Custom encryption algorithms (use standard libraries)
  • Rolling your own authentication (use established providers)
  • Security theater (WAFs without proper input validation)
  • Penetration testing before you've done the basics above

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.


Enjoyed this guide? Subscribe to DevTools Guide — a free weekly newsletter covering developer tools, workflows, and best practices.

More from this blog

DevTools Guide

183 posts