160 lines
6.1 KiB
Markdown
160 lines
6.1 KiB
Markdown
# Full-Stack Application Security Hardening Playbook
|
||||
|
|
|
|||
|
|
This high-level guide provides a reusable, standardized protocol for auditing and securing full-stack applications (Next.js, Node.js, Express, etc.).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 1: Endpoint & Access Control (Preventing IDOR)
|
|||
|
|
|
|||
|
|
### 1. The Multi-Tenant Ownership Rule
|
|||
|
|
Every state-changing API request (`POST`, `PUT`, `PATCH`, `DELETE`) and private resource read (`GET`) must establish identity via the server session—never by trusting client parameters.
|
|||
|
|
|
|||
|
|
#### Never Trust Client User IDs:
|
|||
|
|
```typescript
|
|||
|
|
// ❌ Vulnerable (attacker can pass any userId in body)
|
|||
|
|
const { title, userId } = await req.json();
|
|||
|
|
await db.resource.create({ data: { title, userId } });
|
|||
|
|
|
|||
|
|
// ✅ Secure (always bind to verified session)
|
|||
|
|
const session = await getServerSession(authOptions);
|
|||
|
|
if (!session?.user?.id) return unauthorized();
|
|||
|
|
await db.resource.create({ data: { title, userId: session.user.id } });
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Verify Resource Ownership Before Mutation:
|
|||
|
|
```typescript
|
|||
|
|
// Verify target record exists AND belongs to the caller
|
|||
|
|
const item = await db.resource.findUnique({ where: { id } });
|
|||
|
|
if (!item) return notFound();
|
|||
|
|
if (item.userId !== session.user.id) return forbidden();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Lock Down Sensitive Data Endpoints:
|
|||
|
|
* Strip internal data (emails, password hashes, roles, permissions) from public query endpoints.
|
|||
|
|
* Never allow query flags like `?includePrivate=true` unless the caller has verified ownership or administrative authorization for that account.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 2: Input Boundaries & Payload Sanitization
|
|||
|
|
|
|||
|
|
### 1. Enforce Hard Limits on Every Field
|
|||
|
|
Unbounded input strings can cause database exhaustion, buffer overruns, or CPU denial-of-service (e.g., Bcrypt hashing huge strings).
|
|||
|
|
|
|||
|
|
| Field Type | Recommended Hard Bound | Rationale |
|
|||
|
|
| :--- | :--- | :--- |
|
|||
|
|
| **Passwords** | Min 8 chars, Max 72–100 chars | Bcrypt truncates or burns excessive CPU beyond 72 bytes |
|
|||
|
|
| **Emails** | Max 254 chars | Standard RFC 5321 length; prevents regex DoS |
|
|||
|
|
| **Titles / Names** | Max 100–200 chars | Prevents UI overflow and DB memory bloat |
|
|||
|
|
| **Descriptions / Content** | Max 2,000–50,000 chars | Prevents payload bombing |
|
|||
|
|
| **Pagination limit** | Clamped to `1 <= limit <= 50` | Prevents querying the entire table at once |
|
|||
|
|
|
|||
|
|
### 2. JSON & Batch Array Clamping
|
|||
|
|
When accepting array batches (e.g., bulk updating nodes or items), enforce an explicit upper limit (e.g., max 200–500 items per request).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 3: Safe File & Document Upload Architecture
|
|||
|
|
|
|||
|
|
If an application accepts images, PDFs, or documents, implement the **Four-Pillar Upload Defense**:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
A["File Upload"] --> B["1. Size Check"]
|
|||
|
|
B --> C["2. Extension & MIME Allowlist"]
|
|||
|
|
C --> D["3. Magic Bytes Inspection"]
|
|||
|
|
D --> E["4. Filename Sanitization & Storage"]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 1. Size Caps:
|
|||
|
|
* Check file size before buffering: images (max 2–5MB), documents (max 10MB).
|
|||
|
|
* Terminate stream early if incoming chunks exceed the configured byte threshold.
|
|||
|
|
|
|||
|
|
### 2. Strict MIME & Extension Allowlists:
|
|||
|
|
* **Allowed:** `image/jpeg` (`.jpg`, `.jpeg`), `image/png` (`.png`), `image/webp` (`.webp`), `application/pdf` (`.pdf`), `application/json` (`.json`), `text/markdown` (`.md`).
|
|||
|
|
* **Strictly Blocked:** `.exe`, `.bat`, `.sh`, `.php`, `.js`, `.html`, `.svg` (SVGs can embed `<script>` tags and execute XSS unless explicitly sanitized).
|
|||
|
|
|
|||
|
|
### 3. Magic Bytes (Header Signature) Inspection:
|
|||
|
|
Do not trust the client `Content-Type` header or file extension alone. Inspect the initial bytes of the file buffer:
|
|||
|
|
|
|||
|
|
| Format | Magic Bytes Signature | Hex Representation |
|
|||
|
|
| :--- | :--- | :--- |
|
|||
|
|
| **JPEG** | `\xFF\xD8\xFF` | `FF D8 FF` |
|
|||
|
|
| **PNG** | `\x89PNG\r\n\x1a\n` | `89 50 4E 47` |
|
|||
|
|
| **WebP** | `RIFF....WEBP` | `52 49 46 46` ('RIFF') |
|
|||
|
|
| **PDF** | `%PDF-` | `25 50 44 46` ('%PDF') |
|
|||
|
|
|
|||
|
|
### 4. Filename Sanitization & Non-Executable Storage:
|
|||
|
|
* Strip directory traversal sequences (`../`, `..\\`) and null bytes (`\0`).
|
|||
|
|
* Store files using randomized UUIDs/CUIDs (e.g., `clx987654321.png`) rather than the original user-supplied name.
|
|||
|
|
* Serve uploaded files with `Content-Disposition: attachment` or host from dedicated static object storage (S3/GCS) where server script execution is disabled.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 4: Rate Limiting & Abuse Prevention
|
|||
|
|
|
|||
|
|
### Protect Sensitive Action Gates:
|
|||
|
|
* **Login attempts:** Max 5–10 per 15 minutes per IP.
|
|||
|
|
* **User registrations:** Max 3–5 per hour per IP.
|
|||
|
|
* **Password resets:** Max 3 per hour per email/IP.
|
|||
|
|
* **General API read/write:** 60–120 requests per minute per authenticated user / IP.
|
|||
|
|
|
|||
|
|
### Implementation Strategy:
|
|||
|
|
* **Single Server / Microservice:** Use an in-memory sliding-window bucket with automatic timestamp cleanup.
|
|||
|
|
* **Distributed / Serverless:** Use Redis (e.g., Upstash / Redis token bucket) to share rate limits across multiple instances.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 5: Framework Hardening & HTTP Security Headers
|
|||
|
|
|
|||
|
|
### 1. Disable Fingerprinting
|
|||
|
|
* In `next.config.ts` / `next.config.js`:
|
|||
|
|
```typescript
|
|||
|
|
const nextConfig = {
|
|||
|
|
poweredByHeader: false,
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
* In Express:
|
|||
|
|
```typescript
|
|||
|
|
app.disable('x-powered-by');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Standard Security Headers
|
|||
|
|
Inject these headers across all routes (via Next.js `headers()` config, middleware, or reverse proxy):
|
|||
|
|
|
|||
|
|
```http
|
|||
|
|
X-Frame-Options: SAMEORIGIN
|
|||
|
|
X-Content-Type-Options: nosniff
|
|||
|
|
Referrer-Policy: strict-origin-when-cross-origin
|
|||
|
|
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
|
|||
|
|
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Phase 6: Continuous Dependency & Build Verification
|
|||
|
|
|
|||
|
|
Establish a 4-step security check before every release:
|
|||
|
|
|
|||
|
|
### 1. Vulnerability Audit:
|
|||
|
|
```bash
|
|||
|
|
npm audit
|
|||
|
|
# Apply automated non-breaking security fixes:
|
|||
|
|
npm audit fix
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Static Code & Lint Analysis:
|
|||
|
|
```bash
|
|||
|
|
npm run lint
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. Automated Security Unit Tests:
|
|||
|
|
Maintain unit tests validating that:
|
|||
|
|
* File upload allowlists reject `.exe`, `.html`, `.svg`, and mismatched magic bytes.
|
|||
|
|
* Rate limiters trigger HTTP 429 when thresholds are exceeded.
|
|||
|
|
* Boundary helpers clamp oversized numbers and truncate/reject excessive payload strings.
|
|||
|
|
* Endpoints reject unauthenticated mutations and IDOR resource manipulation.
|
|||
|
|
|
|||
|
|
### 4. Production Build Verification:
|
|||
|
|
```bash
|
|||
|
|
npm run build
|
|||
|
|
```
|