SuperAPI AI Directory

security-and-hardening

Security & auditing

View repository

Updated 10-16 09:22

About this skill

Security and hardening

Overview

Security-first development practices for web applications. Treat every external input as hostile, every secret as sacred, and every authorization check as mandatory. Security is not a phase; it is a constraint on every line of code that touches user data, authentication, or external systems.

When to use

  • Building anything that accepts user input
  • Implementing authentication or authorization
  • Storing or transmitting sensitive data
  • Integrating external APIs or services
  • Adding file uploads, webhooks, or callbacks
  • Handling payments or PII data

Process: threat modeling first

Stacking controls on top without a threat model is just guessing. Before hardening, spend five minutes thinking like an attacker:

  1. Draw trust boundaries. Where does untrusted data enter your system? HTTP requests, form fields, file uploads, webhooks, third-party APIs, message queues, and LLM output; also including local values that look internal because the OS hands them to you: another process's command line or environment variables, filenames on shared volumes, paths in job payloads. Trust follows thewriterof a value, not the channel that transmits it. Every boundary is an attack surface.
  2. Name the assets. What is worth stealing or destroying? Credentials, PII, payment data, admin operations, money flows.
  3. Run STRIDE on each boundary — it is a quick lens, not a ritual:
Threat Question to ask Typical mitigation
SSpoofing Can someone impersonate a user/service? Authentication, signature verification
TTampering Can data be tampered with in transit or at rest? Integrity checks, parameterized queries, HTTPS
RRepudiation Can an action later be denied? Audit logging for security events
IInformation disclosure Can data leak? Encryption, field allowlists, generic error messages
DDenial of service Can it be knocked over? Rate limiting, input size caps, timeouts
EElevation of privilege Can a user gain permissions they should not have? Authorization checks, least privilege
  1. Write an abuse case next to each use case. For each feature, ask yourself: "How would I abuse this?" Then turn that answer into your first test.

If you can't state the trust boundaries of a feature, you're not ready to protect it. This is the OWASP A04: Insecure Design — most breaches start in design, not code.

three-layer boundary system

Always Do (no exceptions)

  • Validate all external input at every system boundary (API routes, form handlers)
  • Parameterize all database queries — never concatenate user input into SQL
  • Encode output to prevent XSS (use the framework's automatic escaping, don't bypass it)
  • Use HTTPS for all external communication
  • Hash passwords with bcrypt/scrypt/argon2(never store plaintext)
  • Set security headers(CSP、HSTS、X-Frame-Options、X-Content-Type-Options)
  • Use httpOnly, secure, sameSite cookies for sessions
  • Before every release, run the detected package manager's native audit against the committed lockfile

Ask First (requires human approval)

  • Adding a new authentication flow or modifying auth logic
  • Storing a new category of sensitive data (PII, payment information)
  • Adding a new external service integration
  • Modifying CORS configuration
  • Adding a file upload handler
  • Modifying rate limiting or throttling
  • Granting higher privileges or roles

Never Do

  • Never commit secretsto version control (API keys, passwords, tokens)
  • Never log sensitive data(passwords, tokens, full credit card numbers)
  • Never treat client-side validation as a security boundary
  • Never disable security headers for convenience
  • Never use eval() or innerHTML
  • Never store sessions in client-accessible storage(using localStorage for auth tokens)
  • Never expose stack traces to users or internal error details

OWASP Top 10 protection patterns

These are preventive patterns, not a ranking. For the 2021 ordering, see ../../references/security-checklist.md in the cheat sheet.

Injection(SQL、NoSQL、OS Command)

// BAD: SQL injection via string concatenation
const query = `SELECT * FROM users WHERE id = '${userId}'`;

// GOOD: Parameterized query
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);

// GOOD: ORM with parameterized input
const user = await prisma.user.findUnique({ where: { id: userId } });

Broken Authentication

// Password hashing
import { hash, compare } from 'bcrypt';

const SALT_ROUNDS = 12;
const hashedPassword = await hash(plaintext, SALT_ROUNDS);
const isValid = await compare(plaintext, hashedPassword);

// Session management
app.use(session({
  secret: process.env.SESSION_SECRET,  // From environment, not code
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,     // Not accessible via JavaScript
    secure: true,       // HTTPS only
    sameSite: 'lax',    // CSRF protection
    maxAge: 24 * 60 * 60 * 1000,  // 24 hours
  },
}));

Cross-Site Scripting (XSS)

// BAD: Rendering user input as HTML
element.innerHTML = userInput;

// GOOD: Use framework auto-escaping (React does this by default)
return <div>{userInput}</div>;

// If you MUST render HTML, sanitize first
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);

Broken Access Control

// Always check authorization, not just authentication
app.patch('/api/tasks/:id', authenticate, async (req, res) => {
  const task = await taskService.findById(req.params.id);

  // Check that the authenticated user owns this resource
  if (task.ownerId !== req.user.id) {
    return res.status(403).json({
      error: { code: 'FORBIDDEN', message: 'Not authorized to modify this task' }
    });
  }

  // Proceed with update
  const updated = await taskService.update(req.params.id, req.body);
  return res.json(updated);
});

Security Misconfiguration

// Security headers (use helmet for Express)
import helmet from 'helmet';
app.use(helmet());

// Content Security Policy
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],  // Tighten if possible
    imgSrc: ["'self'", 'data:', 'https:'],
    connectSrc: ["'self'"],
  },
}));

// CORS — restrict to known origins
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
  credentials: true,
}));

Sensitive Data Exposure

// Never return sensitive fields in API responses
function sanitizeUser(user: UserRecord): PublicUser {
  const { passwordHash, resetToken, ...publicFields } = user;
  return publicFields;
}

// Use environment variables for secrets
const API_KEY = process.env.STRIPE_API_KEY;
if (!API_KEY) throw new Error('STRIPE_API_KEY not configured');

Server-Side Request Forgery (SSRF)

Whenever the server fetches a user-influenced URL — webhooks, "import from URL", image proxies, link previews — an attacker can point it at internal services (cloud metadata,localhost, private IPs).

// BAD: fetch whatever the user gives you
await fetch(req.body.webhookUrl);

// GOOD: allowlist scheme + host, reject if ANY resolved IP is private, forbid redirects
import { lookup } from 'node:dns/promises';
import ipaddr from 'ipaddr.js';

const ALLOWED_HOSTS = new Set(['hooks.example.com']);

async function assertSafeUrl(raw: string): Promise<URL> {
  const url = new URL(raw);
  if (url.protocol !== 'https:') throw new Error('https only');
  if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error('host not allowed');
  // Resolve ALL records; a single private/reserved address fails the check.
  const addrs = await lookup(url.hostname, { all: true });
  if (addrs.some((a) => ipaddr.parse(a.address).range() !== 'unicast')) {
    throw new Error('private/reserved IP');
  }
  return url;
}

await fetch(await assertSafeUrl(req.body.webhookUrl), { redirect: 'error' });

range() !== 'unicast' This check covers loopback, link-local 169.254.169.254(cloud metadata, SSRF's number one target), private and unique-local ranges, for both IPv4 and IPv6.

Note — there is still a TOCTOU gap here. fetch re-resolves DNS after the check, so an attacker using a record with a short TTL can rebind the domain to an internal IP between validation and connection establishment. For high-risk scenarios, either resolve once and connect to a pinned IP, or put a filtering proxy in front (request-filtering-agent / ssrf-req-filter)。

Input validation patterns

Schema Validation at the boundary

import { z } from 'zod';

const CreateTaskSchema = z.object({
  title: z.string().min(1).max(200).trim(),
  description: z.string().max(2000).optional(),
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
  dueDate: z.string().datetime().optional(),
});

// Validate at the route handler
app.post('/api/tasks', async (req, res) => {
  const result = CreateTaskSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Invalid input',
        details: result.error.flatten(),
      },
    });
  }
  // result.data is now typed and validated
  const task = await taskService.create(result.data);
  return res.status(201).json(task);
});

File upload security

// Restrict file types and sizes
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB

function validateUpload(file: UploadedFile) {
  if (!ALLOWED_TYPES.includes(file.mimetype)) {
    throw new ValidationError('File type not allowed');
  }
  if (file.size > MAX_SIZE) {
    throw new ValidationError('File too large (max 5MB)');
  }
  // Don't trust the file extension — check magic bytes if critical
}

Destructive operations on derived paths

The safety of a delete, move or overwrite depends only on the safety of the value that names the target. Reading this value from the kernel, a job payload or a sibling service only proves where itcomes from, not whowritten— another process's command line and form fields are just as attacker-controlled. A shape check ("absolute path, at least one directory deep") only proves well-formedness, yet is often mistaken for authorization; that is how a cleanup script deletes the root directory instead of a leaf.

Before making a destructive call, three conditions are indispensable: the resolved target lies under some allowlisted root (compare after resolving symlinks, never compare on the raw string); it is at least under that rootone level, so the root itself can never be the target; and it carriesbelongs to youevidence, and this evidence must be read before the operationbefore, before any teardown that would remove it — otherwise "does not exist" and "is not mine" cannot be distinguished. When refusing to execute, record the rejected target and then stop: a cleanup script that falls back to a broader default path is exactly the accident this set of checks is meant to prevent. For a complete example, see ../../references/security-checklist.md。

There are two limitations, because this check looks stronger than it actually is. A marker file inside the tree is self-declared — anything that can write there can write this marker — so the expected owner must come from authenticated state, and the marker itself needs integrity protection (strict ownership or a MAC) to count as authorization. Also, resolving the path first and then operating on thenamecreates a check/use race as long as any untrusted process can swap out an ancestor directory: on a shared volume, hold the target with a file descriptor and use operations that do not follow symlinks and are confined under the root, or ensure the directory hierarchy does not change during this period.

Triage depends on audit results

A package manager's audit reports known advisories; it neither proves a package is trustworthy nor proves the vulnerable code is reachable. Use this decision tree:

The native package-manager audit reports a vulnerability
├── Severity: critical or high
│   ├── Is the vulnerable code reachable in runtime, build, test, or deployment paths?
│   │   ├── YES --> Fix immediately (update, patch, or replace the dependency)
│   │   └── NO (confirmed unused across those paths) --> Fix soon, but not a blocker
│   └── Is a fix available?
│       ├── YES --> Update to the patched version
│       └── NO --> Check for workarounds, consider replacing the dependency, or add to allowlist with a review date
├── Severity: moderate
│   ├── Reachable in production? --> Fix in the next release cycle
│   └── Dev-only? --> Fix when convenient, track in backlog
└── Severity: low
    └── Track and fix during regular dependency updates

Key questions:

  • Is the vulnerable function actually called in your code path?
  • Is this dependency a runtime dependency or only used for development?
  • Given your deployment context, is this vulnerability exploitable (for example, a server-side vulnerability in a client-only application)?

When you defer a fix, record the reason and set a review date.

Supply chain hygiene

Do not assume npm, and do not treat the nearest manifest as the install root. Proceed in this order:

  1. Find the installation boundary and manager. Use the workspace root that owns the lockfile; use it only when an independent nested project lies outside that workspace. There, cross-validate packageManager(when present), the lockfile, and CI; stop when there are disagreements or competing lockfiles. Pin the manager version, and use the ../../references/security-checklist.md comparison matrix in .
  2. Block dependency scripts before first execution. Bootstrap with scripts disabled, or adopt a documented fail-closed policy; inspect the source of pending scripts, approve only the minimum necessary packages, commit this policy, and then verify with a clean frozen/immutable install. Never approve scripts indiscriminately.

Audit can only find known advisories; it cannot catch newly emerging malicious packages or typosquatting packages. Therefore:

  • Never automatically apply forced audit fixes(npm audit fix --force or equivalent command). Preview the fixes first, read the changelogs, and test the resulting upgrades one by one; forcing fixes may go beyond the declared dependency ranges.
  • Verify registry signatures and provenance where supported(npm audit signatures、pnpm audit signatures), and treat "missing" as a signal that needs investigation, not as automatic proof of compromise.
  • Review new dependencies, lockfile diffs, and script-policy changes together — ownership, maintenance status, release age, provenance, transitive dependency graph, and typosquats, such as cross-env compared with crossenv(OWASP A06、LLM03)。

Rate Limiting

import rateLimit from 'express-rate-limit';

// General API rate limit
app.use('/api/', rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                   // 100 requests per window
  standardHeaders: true,
  legacyHeaders: false,
}));

// Stricter limit for auth endpoints
app.use('/api/auth/', rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,  // 10 attempts per 15 minutes
}));

Once the process count exceeds one, use shared storage for counting. express-rate-limit By default, the counter is kept in process memory. Behind a load balancer, each instance counts separately, so the effective limit is max × instances; in serverless or edge runtimes, every new invocation starts from zero, so the auth limit above may never trigger. Pass in a shared store(via rate-limit-redis with Redis), or use an HTTP-based limiter that also works where long-lived TCP connections are unsuitable (for example, @upstash/ratelimit):

import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const authLimiter = new Ratelimit({
  redis: Redis.fromEnv(),                       // UPSTASH_REDIS_REST_URL + _TOKEN
  limiter: Ratelimit.slidingWindow(10, '15 m'), // 10 attempts per 15 minutes, across all instances
});
const { success } = await authLimiter.limit(`login:${req.ip}`);
if (!success) return res.status(429).end();

Secrets management

.env files:
  ├── .env.example  → Committed (template with placeholder values)
  ├── .env          → NOT committed (contains real secrets)
  └── .env.local    → NOT committed (local overrides)

.gitignore must include:
  .env
  .env.local
  .env.*.local
  *.pem
  *.key

Always check before committing:

# Check for accidentally staged secrets
git diff --cached | grep -i "password\|secret\|api_key\|token"

If a secret has already been committed, rotate it immediately. Deleting that line or rewriting history is not enough — if it ever reached a remote, treat it as compromised. Revoke and reissue the key first, then purge it from history.

Data privacy and compliance

Protecting data asks, "Can an attacker read it?" Privacy asks, "Should webeholding it, and for how long?" — this is another question that hardening cannot answer. The data that is easiest to protect, least likely to leak, and cheapest to comply with is the data you never collected. Treat personal data as a liability to minimize, not an asset to hoard.

Know what you hold. You cannot protect or delete data you cannot find. Classify it when you add the field:

Category Example Handling
Non-personal Aggregated data, anonymous counts Routine handling
Personal (PII) Name, email, IP, device/user ID Minimize, access control, include in export/deletion
Sensitive Health, financial, location, biometrics, government ID, any information about minors Collection requires an additional basis, stricter access, and usually encryption + audit logs

Operating rules:

  • Minimize and set a purpose. Collect a field only when there is a clear use for it. "It might be useful later" is not a purpose; it is a potential breach scope. Do not write PII into telemetry logs (observability-and-instrumentation the skill expresses the same point from an ops perspective).
  • Set retention periods in advance, and actually delete. Every personal data store needs a TTL and a working deletion path — including backups, caches, search indexes, and analytics copies. Data with no expiration date is a breach scheduled for the future.
  • Support the data subject rights required by your jurisdiction(GDPR/CCPA and similar regulations): export, correct, and delete on request. These are engineering features — when designing the schema, make the user's datafindable、erasable, rather than irreversibly smeared across systems.
  • Obtain consent before collection or third-party sharing, and make consent auditable. Sending PII to analytics/advertising/LLM vendors counts as "sharing" — let the user's choice determine whether it is allowed, and the vendor needs to sign a data processing agreement.
  • Defaults should be localized; do not hardcode the law of one region. Data residency and rules differ by the user's location; make policy a configurable boundary, not an assumption.

When data crosses a trust boundary, treat it as untrusted data and validate it (see Input Validation above); when a privacy incident exposes personal data, the breach-notification countdown is part of the postmortem — follow debugging-and-error-recovery skill。

AI / LLM feature safety

If your application calls an LLM — chatbots, summarizers, agents, RAG — it inherits a new attack surface. Map it to OWASP Top 10 for LLM Applications (2025):

  • Treat all model output as untrusted input (LLM05: Improper Output Handling). Never pass LLM output directly into eval、SQL、shell、innerHTML or file paths. Validate and encode it like raw user input.
  • Assume prompts may be hijacked (LLM01: Prompt Injection). Any untrusted text in the context window — user messages, scraped web pages, PDFs — may carry instructions. The system prompt is not a security boundary; enforce permissions in code, not in the prompt.
  • Keep secrets and other users' data away from prompts (LLM02 / LLM07). Anything in the context may be echoed back. Do not put API keys, cross-tenant data, or the full system prompt anywhere the model can repeat it.
  • Constrain the permissions of tools and agents (LLM06: Excessive Agency). Limit tools to the minimum scope, require confirmation for destructive or irreversible operations, and validate every tool parameter.
  • Limit consumption (LLM10: Unbounded Consumption). Set upper bounds on tokens, request rate, and loop/recursion depth so that carefully crafted input cannot drive up costs or hang the system.
  • Isolate retrieved data (LLM08: Vector and Embedding Weaknesses). In RAG, treat the vector store as a trust boundary: partition embeddings by tenant so one user cannot retrieve another's data; validate documents before indexing them so poisoned content cannot sway the answers.
// BAD: trusting model output as a command or as markup
const sql = await llm.generate(`Write SQL for: ${userQuestion}`);
await db.query(sql);                                   // arbitrary query execution
container.innerHTML = await llm.reply(userMessage);   // stored XSS, via the model

// GOOD: model output is data — parse defensively, then validate, then encode
let intent;
try {
  intent = CommandSchema.parse(JSON.parse(await llm.replyJson(userMessage)));
} catch {
  throw new ValidationError('unexpected model output'); // JSON.parse or schema failed
}
await runAllowlistedAction(intent.action, intent.params);
container.textContent = await llm.reply(userMessage);

Security Review Checklist

### Authentication
- [ ] Passwords hashed with bcrypt/scrypt/argon2 (salt rounds ≥ 12)
- [ ] Session tokens are httpOnly, secure, sameSite
- [ ] Login has rate limiting
- [ ] Password reset tokens expire

### Authorization
- [ ] Every endpoint checks user permissions
- [ ] Users can only access their own resources
- [ ] Admin actions require admin role verification

### Input
- [ ] All user input validated at the boundary
- [ ] SQL queries are parameterized
- [ ] HTML output is encoded/escaped
- [ ] Server-side URL fetches are allowlisted (no SSRF to internal services)
- [ ] Delete/move/overwrite targets built from data are checked against an allowlisted root, a minimum depth, and ownership evidence read before the operation

### Data
- [ ] No secrets in code or version control
- [ ] Sensitive fields excluded from API responses
- [ ] PII encrypted at rest (if applicable)
- [ ] Personal data is classified, collected against a stated purpose, and minimized
- [ ] Personal data has a retention limit and a working deletion path (incl. backups/indexes)
- [ ] Export/delete (data-subject) requests are supported where required; sharing with third parties has consent

### Infrastructure
- [ ] Security headers configured (CSP, HSTS, etc.)
- [ ] CORS restricted to known origins
- [ ] Dependencies audited for vulnerabilities
- [ ] Error messages don't expose internals

### Supply Chain
- [ ] One authoritative lockfile committed; CI uses that manager's frozen/immutable install
- [ ] Native audit triaged by reachability and fix risk; dependency install scripts blocked unless explicitly approved
- [ ] New dependencies reviewed (ownership, provenance, release age, transitive graph)

### AI / LLM (if used)
- [ ] Model output treated as untrusted (no eval/SQL/innerHTML/shell)
- [ ] Secrets and other users' data kept out of prompts
- [ ] Tool/agent permissions scoped; destructive actions require confirmation

See Also

For detailed security checklists and pre-commit verification steps, see ../../references/security-checklist.md。

Common Rationalizations

Rationalization Reality
"This is an internal tool, security doesn't matter" Internal tools get compromised too. Attackers go for the weakest link.
"We'll add security later" Adding security after the fact is 10 times harder than building it in from the start. Add it now.
"No one will exploit this" Automated scanners will find it. Security through obscurity is not security.
"The framework will handle security" Frameworks provide tools, not guarantees. You still have to use them correctly.
"It's just a prototype" Prototypes ship to production. Security habits start on day one.
"Threat modeling is overkill here" Five minutes thinking "how would I attack this" can prevent design flaws that no control can fix after the fact.
"It's just LLM output, it's only text" That "text" could be a SQL statement, a script tag, or a shell command. Treat it like any untrusted input.
"The audit passed, so the dependencies are safe" Audits match known advisories. They don't detect newly emerged malicious packages, and they don't make an unreviewed install script safe to execute.
"Collect it first, it might be useful later" Data you don't hold can't be breached, subpoenaed, or accidentally deleted. "Might be useful" is a breach scope, not a purpose.
"We handle deletion requests manually" Manual erasure misses backups, caches, and analytical copies. If the schema can't find a given user's data, you can't fulfill a deletion request — design for it.
"Compliance is legal's problem, not ours" Export, deletion, retention, and consent are all schema and code matters. Once you've smeared PII across ten systems, legal can't patch it back together.

Red flags

  • User input passed directly into database queries, shell commands, or HTML rendering
  • The target of a delete, move, or overwrite comes from a payload, a config value, or another process's command line, protected only by a single path-shape check
  • Secrets in source code or commit history
  • API endpoints without authentication or authorization checks
  • Missing CORS configuration, or use of a wildcard (*)origins
  • Auth endpoints without rate limiting, or an in-memory limiter in front of multiple instances
  • Stack traces or internal errors exposed to users
  • Dependencies with known critical vulnerabilities, competing lockfiles in the same installation boundary, non-reproducible installs, scripts approved indiscriminately
  • The server fetches user-provided URLs without an allowlist (SSRF)
  • LLM/model output passed into queries, the DOM, shell, or eval
  • Secrets, PII, or the full system prompt placed into the LLM context window
  • Collecting personal data without a stated purpose, retention period, or deletion path
  • Sending PII to analytics/advertising/LLM vendors without consent or a data processing agreement
  • "Delete my account" only flips a flag, while personal data remains in every store and backup

Verification

After implementing security-related code:

  • The native audit has no unmitigated and reachable critical/high findings; CI retains an authoritative lockfile and blocks unreviewed dependency scripts
  • No secrets in source code or git history
  • All user input is validated at the system boundary
  • Destructive filesystem operations resolve symlinks first, then verify an allowlisted root, minimum depth, and ownership before execution
  • Every protected endpoint checks authentication and authorization
  • Security headers are present in responses (check with browser DevTools)
  • Error responses do not expose internal details
  • Rate limiting is enabled on auth endpoints and backed by shared storage when multiple instances serve traffic
  • Server-side URL fetching is validated against an allowlist (no SSRF)
  • LLM/model output is validated and encoded before use (if AI features are used)
  • Personal data is classified, collected minimally for the stated purpose, and has a retention period
  • Deletion and export requests work end-to-end (including backups, caches, and analytics copies)