diff --git a/.claude/article-brief-template.md b/.claude/article-brief-template.md index f40e940..3b7ecd1 100644 --- a/.claude/article-brief-template.md +++ b/.claude/article-brief-template.md @@ -60,6 +60,17 @@ Use this template when planning new blog articles for Node.js Design Patterns. --- +### Writing Style Checklist + +- [ ] No em dashes (—) used anywhere +- [ ] Context provided before diving into implementation details +- [ ] Explains "why" not just "how" for each concept +- [ ] Friendly, conversational tone throughout +- [ ] Sections connected with transitions (forward/backward references) +- [ ] Technical explanations start with the problem/use case + +--- + ### Internal Links Link to related existing articles: diff --git a/.claude/constitution.md b/.claude/constitution.md index 35e5e0b..b26d364 100644 --- a/.claude/constitution.md +++ b/.claude/constitution.md @@ -41,6 +41,34 @@ We adhere to a set of coding standards to ensure code quality and maintainabilit The repository uses well-defined formatting rules through [Prettier](https://prettier.io/) and linting through [ESLint](https://eslint.org/). +### Content Writing Style + +When creating or editing written content (blog articles, documentation, tutorials), follow these style guidelines: + +#### Tone and Readability +- Use a **friendly, approachable tone** - almost colloquial, as if explaining to a colleague +- Write in a conversational style while maintaining technical accuracy +- Avoid overly formal or academic language + +#### Punctuation and Formatting +- **Never use em dashes (—)** - use commas, parentheses, or separate sentences instead +- Use short paragraphs and clear sentence structure +- Break up long explanations with code examples, lists, or callouts + +#### Structure and Flow +- **Always provide context first** - explain the "what" and "why" before diving into "how" +- **Emphasize the "why"** - readers should understand not just how something works, but why it matters and why it works that way +- **Connect sections smoothly** using: + - Forward references: "In the next section, we'll see how to handle errors" + - Backward references: "As we saw earlier, streams provide memory efficiency" + - Brief previews of what's coming: "Before we implement this, let's understand the underlying concept" + +#### Technical Explanations +- Start with the problem or use case before introducing the solution +- Explain concepts progressively - simple version first, then edge cases and advanced usage +- Use real-world analogies when explaining abstract concepts +- Include practical examples that readers can relate to their own projects + ## Deployment The website is automatically deployed to GitHub Pages through a GitHub Actions workflow. The deployment process is triggered on every push to the `main` branch. The workflow builds the static site and publishes it to GitHub Pages, making it accessible to users. Contributors do not need to manually handle deployment - focus on code quality and the automated pipeline will handle the rest. diff --git a/.claude/content-calendar.md b/.claude/content-calendar.md index 8bc8615..727a5c9 100644 --- a/.claude/content-calendar.md +++ b/.claude/content-calendar.md @@ -11,6 +11,7 @@ 1. **Node.js Core APIs & Built-ins** - High-volume foundational topics 2. **Modern Node.js Features** - New capabilities (22+, 23+) with early-mover SEO advantage 3. **Node.js Patterns & Architecture** - Advanced patterns (light connection to book) +4. **Node.js Security** - Security best practices and vulnerability prevention --- @@ -52,12 +53,21 @@ | 15 | NOT STARTED | Import maps in Node.js | "node js import maps" | 1,000+ | Modern | | 16 | NOT STARTED | Encrypting files with Node.js | "node js encrypt file" | 1,500+ | Core APIs | -### Month 9-12: Advanced & Experimental +### Month 9-10: Security + +| # | Status | Article | Primary Keyword | Est. Volume | Pillar | +| --- | -------- | --------------------------------- | --------------------------------- | ----------- | -------- | +| 17 | COMPLETE | **Path Traversal Security Guide** | "node js path traversal" | 1,000+ | Security | +| 18 | NOT STARTED | Input Validation in Node.js | "node js input validation" | 2,000+ | Security | +| 19 | NOT STARTED | Secure File Uploads | "node js secure file upload" | 1,500+ | Security | +| 20 | NOT STARTED | OWASP Top 10 for Node.js | "node js security best practices" | 3,000+ | Security | + +### Month 11-12: Advanced & Experimental | # | Status | Article | Primary Keyword | Notes | | --- | ----------- | ------------------------------ | --------------- | --------------------- | -| 17+ | NOT STARTED | Rust/Zig + Node.js integration | niche | Thought leadership | -| 18+ | NOT STARTED | TBD based on analytics | TBD | Iterate based on data | +| 21+ | NOT STARTED | Rust/Zig + Node.js integration | niche | Thought leadership | +| 22+ | NOT STARTED | TBD based on analytics | TBD | Iterate based on data | --- @@ -69,6 +79,7 @@ | Stream consumers | COMPLETE | /blog/node-js-stream-consumer | | Async iterators | COMPLETE | /blog/javascript-async-iterators | | Race conditions | COMPLETE | /blog/node-js-race-conditions | +| Path traversal security | COMPLETE | /blog/nodejs-path-traversal-security | | Checking Node.js version | COMPLETE | /blog/checking-node-js-version | | Installing Node.js | UPDATED | /blog/5-ways-to-install-node-js | | Docker development | COMPLETE | /blog/node-js-development-with-docker-and-docker-compose | @@ -88,6 +99,7 @@ Environment Variables Files & Paths └── links to → Reading/Writing Files (existing) └── links to → Hashing Files + └── links to → Path Traversal Security (existing) └── links to → Encrypting Files Streams Guide @@ -102,6 +114,20 @@ Event Emitter Installing Node.js └── links to → Checking Node.js version (existing) └── links to → Docker development (existing) + +Path Traversal Security (NEW) + └── links to → Reading/Writing Files (existing) + └── links to → Race Conditions (existing) + └── links to → 5 Ways to Install Node.js (existing) + └── links to → Input Validation (future) + └── links to → Secure File Uploads (future) + └── links to → OWASP Top 10 for Node.js (future) + +Security Cluster (future) + └── Path Traversal Security (hub for file security) + └── Input Validation + └── Secure File Uploads + └── OWASP Top 10 for Node.js (potential hub) ``` --- @@ -112,8 +138,11 @@ Installing Node.js - [x] Update "5 Ways to Install Node.js" with fnm, Volta, Docker - [x] Cross-link existing articles - [x] Add CTAs for free chapter download to all posts +- [x] Publish path traversal security article (new security pillar) +- [x] Add link to path traversal article from Reading/Writing Files guide - [ ] Monitor keyword rankings for existing content - [ ] A/B test CTA placements +- [ ] Promote path traversal article on security forums (r/netsec, HN) --- @@ -123,3 +152,6 @@ Installing Node.js - Most competing articles still recommend axios/got first - opportunity to differentiate - New Node.js features (22+, 23+) have early-mover advantage - Light book excerpts where relevant (Ch 6 streams, Ch 10 testing) +- **Security content** differentiates us from typical tutorial sites and builds trust/authority +- Path traversal article targets developers building file servers, image handlers, and upload systems +- Security topics have strong CVE/news hooks for promotion and backlinks diff --git a/.claude/instructions.md b/.claude/instructions.md index b09bb76..203a4e2 100644 --- a/.claude/instructions.md +++ b/.claude/instructions.md @@ -9,6 +9,7 @@ Key principles to remember: - Maintain accessibility standards (semantic HTML, ARIA, keyboard navigation) - Use data-driven approach with strongly typed content collections - Follow established coding standards with ESLint and Prettier +- Follow content writing style guidelines (friendly tone, context-first, explain the "why") - All changes are automatically deployed via GitHub Actions to GitHub Pages Always read the constitution file when starting work to ensure alignment with project principles. diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bfa4855..812f978 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -43,7 +43,8 @@ "mcp__playwright__browser_network_requests", "mcp__playwright__browser_close", "mcp__playwright__browser_resize", - "mcp__playwright__browser_run_code" + "mcp__playwright__browser_run_code", + "Skill(marketing-skills:seo-audit)" ] } } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e0fc71e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,122 @@ +# AGENTS.md + +This file provides guidance to AI coding assistants when working with code in this repository. + +## Project Overview + +Official website for the book "Node.js Design Patterns" by Mario Casciaro and Luciano Mammino. Built with Astro as a minimal static site, deployed to GitHub Pages at https://nodejsdesignpatterns.com. + +## Commands + +```bash +pnpm dev # Start dev server at localhost:4321 +pnpm build # Build production site to ./dist/ +pnpm preview # Preview production build locally +pnpm lint # Run ESLint +pnpm lint:fix # Fix auto-fixable ESLint issues +pnpm format # Format with Prettier +pnpm format:check # Check formatting +pnpm typecheck # TypeScript type checking +``` + +## Architecture + +### Core Stack + +- **Astro 5** - Static site generator (ESM, TypeScript) +- **Tailwind CSS 4** - Styling via Vite plugin +- **React** - Used sparingly for interactive components only +- **pnpm** - Package manager + +### Content Collections (`src/content/`) + +Strongly typed via Zod schemas in `src/content.config.ts`: + +- `authors/` - Author JSON files with profile pics +- `blog/` - Blog posts (markdown with frontmatter, each in its own folder) +- `chapters/` - Book chapter descriptions +- `faq/` - FAQ entries +- `quotes/` - Testimonials +- `reviews/` - Book reviews + +### Key Directories + +- `src/components/` - Astro components (`.astro`) and React components (`.tsx`) +- `src/components/blog/` - Blog-specific components including `BlogLayout.astro` +- `src/components/ui/` - Reusable UI components (badge, button, card) +- `src/pages/` - Astro routes (index, blog, RSS, 404) +- `src/plugins/` - Remark plugins (admonitions for tip/note/warning/etc.) +- `src/lib/` - Utilities, constants, theme configuration +- `src/images/` - Images optimized by Astro + +### Markdown Features + +Blog posts support admonitions via remark-directive: + +```markdown +:::tip[Custom Title] +Content here +::: +``` + +Types: `tip`, `note`, `important`, `caution`, `warning` + +## Development Principles + +1. **Astro-first**: Prefer Astro components over React. Use React only for interactive features that require client-side JS. + +2. **Mobile-first responsive**: Use Tailwind breakpoint prefixes (`sm:`, `md:`, `lg:`) starting from mobile layouts. + +3. **Accessibility required**: Semantic HTML, proper heading hierarchy, ARIA labels, keyboard navigation, WCAG AA contrast. + +4. **Data-driven content**: Separate data from templates. Use content collections with Zod schemas. + +5. **Lean and fast**: Optimize images, minimize JS, pre-build everything possible. + +## Spec-Driven Development Workflow + +This project uses a spec-driven development workflow: + +1. **Specify** - Create feature branch and specification +2. **Plan** - Generate implementation plan from spec +3. **Tasks** - Break plan into executable tasks + +Templates are in `templates/` and helper scripts in `scripts/`. + +## Blog Article Creation + +New blog articles should: + +- Follow the template in `.claude/article-brief-template.md` +- Use modern ESM syntax and async/await +- Include FAQ section for schema markup +- Place each article in its own folder under `src/content/blog//` + +### Content Writing Style + +When writing or editing content, follow these guidelines: + +**Tone and Readability** + +- Use a friendly, approachable tone, almost colloquial, as if explaining to a colleague +- Write conversationally while maintaining technical accuracy +- Avoid overly formal or academic language + +**Punctuation** + +- Never use em dashes (—). Use commas, parentheses, or separate sentences instead + +**Structure and Flow** + +- Always provide context first: explain "what" and "why" before diving into "how" +- Emphasize the "why": readers should understand not just how something works, but why it matters +- Connect sections smoothly using: + - Forward references: "In the next section, we'll see how to handle errors" + - Backward references: "As we saw earlier, streams provide memory efficiency" + - Previews: "Before we implement this, let's understand the underlying concept" + +**Technical Explanations** + +- Start with the problem or use case before the solution +- Explain progressively: simple version first, then edge cases +- Use real-world analogies for abstract concepts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/src/content/blog/nodejs-path-traversal-security/index.md b/src/content/blog/nodejs-path-traversal-security/index.md new file mode 100644 index 0000000..e9eff57 --- /dev/null +++ b/src/content/blog/nodejs-path-traversal-security/index.md @@ -0,0 +1,863 @@ +--- +date: 2026-02-02T18:17:00 +updatedAt: 2026-02-02T18:17:00 +title: 'Node.js Path Traversal: Prevention & Security Guide' +slug: nodejs-path-traversal-security +description: Learn to prevent path traversal attacks in Node.js. Secure file servers with input validation, boundary checks, and defense-in-depth patterns. +authors: ['luciano-mammino'] +tags: ['blog'] +faq: + - question: What is a path traversal attack in Node.js? + answer: A path traversal attack exploits improper input validation to access files outside the intended directory using sequences like "../" to navigate up the filesystem. In Node.js, this often happens when user input is passed directly to path.join() or fs functions without validation. + - question: How do I prevent path traversal in Node.js? + answer: Prevent path traversal by (1) decoding user input, (2) rejecting absolute paths, (3) resolving paths with path.resolve(), (4) following symlinks with fs.realpath(), and (5) verifying the final path stays within your root directory using startsWith(). + - question: Is path.join() safe from path traversal? + answer: No, path.join() does not prevent path traversal. It simply concatenates paths without security validation. An input like "../../etc/passwd" will be joined as-is, allowing directory escape. Always validate paths after joining. + - question: How do I secure file uploads in Node.js? + answer: Secure file uploads by (1) validating filenames with strict patterns, (2) storing files with generated names rather than user-provided ones, (3) serving files through a validated path resolution function, and (4) using file handles to minimize TOCTOU race conditions. + - question: Does Express.js protect against path traversal? + answer: Express.js provides some built-in protections through res.sendFile(), but you should never rely solely on framework behavior. Always validate paths yourself using techniques like path.resolve(), fs.realpath(), and boundary checking with startsWith() before passing them to any file-serving function. + - question: How do I test for path traversal vulnerabilities? + answer: Test with attack payloads including basic traversal (../), URL encoding (%2e%2e%2f), double encoding (%252e%252e%252f), null bytes (%00), and absolute paths. Use automated tools like OWASP ZAP or Burp Suite, and write unit tests that verify your validation rejects all these patterns. +--- + +Building on our extensive [Node.js File Operations Guide](/blog/reading-writing-files-nodejs/), let's explore one of the most critical security vulnerabilities related to handling files and paths in web applications: **path traversal attacks**. + +It's surprisingly easy to build Node.js applications where users can influence which files get loaded from the filesystem. Think about a simple image server: depending on the URL a user requests, your application decides which file to return. A user requests `/images/cat.jpg`, and your server dutifully streams the file from your uploads directory. But what happens when a malicious user requests `/images/../../etc/passwd` instead? If you're not careful, that request could escape your uploads folder entirely and expose sensitive system files. + +An attacker can craft malicious requests to read configuration files containing database credentials, access private SSH keys, or examine application source code to discover additional vulnerabilities. This information disclosure often becomes the gateway for attackers to move laterally through your infrastructure, escalating what started as a simple web request into a full system compromise. + +Path traversal has been one of the most severely exploited attack vectors in recent years, affecting everything from Apache web servers to popular npm packages. This is not a theoretical concern; it's a real and present danger that deserves your full attention when building production applications. + +In this article, you'll learn exactly what a path traversal attack is, how it happens in practice, and (most importantly) what you must do to build Node.js applications that are not vulnerable. + +## Quick Answer: Secure Path Resolution + +Here's the TLDR; + +If you're already familiar with path traversal attacks and just need a quick checklist to sanity-check your implementation, here's the summary: + +To prevent path traversal in Node.js: + +1. **Fully decode user input** handling double/triple encoding with a loop +2. **Reject null bytes** that can truncate paths +3. **Reject absolute paths** with `path.isAbsolute()` +4. **Reject Windows-specific paths** (drive letters, UNC paths) +5. **Resolve to canonical path** with `path.resolve()` +6. **Follow symlinks** with `fs.realpath()` +7. **Verify path stays within root** using `startsWith(root + path.sep)` + +Here's a possible implementation of all these precautions: + +```js +// safe-resolve.js +import path from 'node:path' +import fs from 'node:fs/promises' + +function fullyDecode(input) { + let result = String(input) + for (let i = 0; i < 10; i++) { + try { + const decoded = decodeURIComponent(result) + if (decoded === result) break + result = decoded + } catch { + // decodeURIComponent throws a URIError on malformed sequences + break + } + } + return result +} + +export async function safeResolve(root, userInput) { + // 1. Fully decode (handles double/triple encoding) + const decoded = fullyDecode(userInput) + + // 2. Reject null bytes + if (decoded.includes('\0')) { + throw new Error('Null bytes not allowed') + } + + // 3. Reject absolute paths + if (path.isAbsolute(decoded)) { + throw new Error('Absolute paths not allowed') + } + + // 4. Reject Windows drive letters and UNC paths + if (/^[a-zA-Z]:/.test(decoded)) { + throw new Error('Drive letters not allowed') + } + if (decoded.startsWith('\\\\') || decoded.startsWith('//')) { + throw new Error('UNC paths not allowed') + } + + // 5. Resolve to canonical path + const safePath = path.resolve(root, decoded) + + // 6. Follow symlinks + const realPath = await fs.realpath(safePath) + + // 7. Verify path stays within root + if (!realPath.startsWith(root + path.sep)) { + throw new Error('Path traversal detected') + } + + return realPath +} +``` + +:::important[Resolve root with realpath at startup] +The `root` parameter should be pre-resolved with `fs.realpath()` at application startup. On systems where the root path contains symlinks (like macOS where `/var` is a symlink to `/private/var`), `fs.realpath()` on user files returns the fully resolved path. If your root isn't also resolved, the `startsWith` check will fail even for valid paths. +::: + +:::warning[Don't Forget Your Dependencies] +Path traversal vulnerabilities can also exist in your dependencies, not just in your own application code. A layered defense approach that combines secure coding practices, regular dependency updates, and vulnerability scanning (using tools like `npm audit`) is essential for maintaining a secure application. +::: + +Don't just copy-paste the snippet above into your app without understanding it. Read on to learn _why_ each of these measures is necessary and how they work together to protect your application. + +## Understanding Path Traversal Vulnerabilities + +### What is a Path Traversal Attack? + +A **path traversal** (also known as **directory traversal**) attack is a security vulnerability that allows an attacker to access files and directories stored outside the intended web root folder. By manipulating variables that reference files with "dot-dot-slash (../)" sequences and variations, an attacker can read arbitrary files on the server. + +### Why Are These Attacks Dangerous? + +Path traversal vulnerabilities can lead to: + +1. **Information Disclosure**: Attackers can read sensitive files like configuration files, database credentials, or private keys. +2. **System Compromise**: In some cases, attackers might access system files that reveal information about the server's architecture. +3. **Data Theft**: Access to application data files could lead to data breaches. +4. **Lateral Movement**: Information gained from these attacks can help attackers plan further attacks on your system. + +### The Anatomy of a Path Traversal Attack + +Path traversal attacks typically follow these steps: + +1. **Identify Vulnerability**: The attacker discovers that user input is used to construct file paths. +2. **Craft Payload**: The attacker creates a payload with directory traversal sequences. +3. **Exploit**: The attacker sends the payload to the server. +4. **Access Files**: If successful, the attacker can now access files outside the intended directory. + +## The Vulnerable Implementation: A Naive Image Server + +Let's begin with a common but vulnerable implementation of an image server: + +```js +// vulnerable-image-server.js +import { createServer } from 'node:http' +import { createReadStream } from 'node:fs' +import path from 'node:path' + +// ⚠️ VULNERABLE: Do not use in production +const server = createServer((req, res) => { + const url = new URL(req.url, `http://${req.headers.host}`) + + // Extract user-provided path (e.g., /images/cats/kitty.jpg) + const rel = url.pathname.replace(/^\/images\//, '') + + // DANGEROUS: Directly joining user input with our directory + const filePath = path.join(process.cwd(), 'uploads', rel) + + // Set content type based on file extension + const ext = path.extname(filePath).toLowerCase() + const type = + ext === '.jpg' || ext === '.jpeg' + ? 'image/jpeg' + : ext === '.png' + ? 'image/png' + : ext === '.gif' + ? 'image/gif' + : 'application/octet-stream' + + const stream = createReadStream(filePath) + stream.once('open', () => { + res.writeHead(200, { 'Content-Type': type }) + stream.pipe(res) + }) + stream.once('error', () => { + res.writeHead(404) + res.end('Image not found') + }) +}) + +server.listen(3000, () => { + console.log('Image server running at http://localhost:3000') +}) +``` + +This server handles requests to `/images/*` by extracting the path after `/images/`, joining it with an `uploads` directory, and streaming the file back to the client. It determines the content type based on the file extension and uses Node.js streams to efficiently serve the file without loading it entirely into memory. + +At first glance, this looks like a reasonable implementation. But there's a critical security flaw lurking in this code. + +### Why This Code is Vulnerable + +Let's break down the security issues in this implementation: + +1. **Unvalidated User Input**: The code directly uses `url.pathname` without any validation. +2. **Unsafe Path Construction**: `path.join()` simply concatenates paths without checking if the result stays within the intended directory. +3. **No Boundary Checking**: There's no verification that the final path is still within the `uploads` directory. +4. **No Input Sanitization**: Special characters like `../` are not filtered or handled safely. + +When a user requests `/images/../../../../etc/passwd`, the code: + +1. Extracts `../../../../etc/passwd` from the URL +2. Joins it with the current working directory and `uploads` +3. Results in a path like `/home/user/myapp/uploads/../../../../etc/passwd` +4. Which resolves to `/etc/passwd`, completely outside our intended directory! + +:::warning[Real-World Impact] +The attack described above allows an attacker to read `/etc/passwd`, which on Unix systems reveals the list of system users. But that's just the beginning. Other ways attackers exploit path traversal vulnerabilities include: + +- Accessing `.env` files containing API keys and database credentials +- Reading private SSH keys from `~/.ssh/id_rsa` +- Examining application source code to discover additional vulnerabilities +- Reading configuration files to understand the system architecture + ::: + +## The Attack: Common Exploitation Techniques + +Now that we understand why our naive implementation is dangerous, let's explore the various techniques attackers use to exploit path traversal vulnerabilities. Understanding these attack vectors helps us build better defenses. + +### Common Attack Vectors + +Path traversal attacks can take many sophisticated forms: + +1. **Basic traversal**: `../../etc/passwd` (the case we have just seen) +2. **URL encoding**: `..%2F..%2Fetc%2Fpasswd` +3. **Double encoding**: `..%252F..%252Fetc%252Fpasswd` +4. **Windows paths**: `..\..\windows\system32\config\sam` +5. **Mixed encoding**: `..%2F..%5Cetc%2Fpasswd` +6. **Overlong UTF-8**: `..%c0%af..%c0%afetc%c0%afpasswd` (largely a legacy attack vector; modern UTF-8 parsers reject these malformed sequences, but older systems may be vulnerable) + +:::tip[URL Decoding Matters] +Many HTTP servers and frameworks decode URL-encoded characters once, but behavior varies by framework. The important point is that validation must happen **after** full URL decoding, not before. Always decode input explicitly rather than relying on framework behavior. +::: + +### Real-World Examples + +Path traversal vulnerabilities have affected many major applications even outside the realm of Node.js. Here are some notable examples: + +1. **Apache HTTP Server ([CVE-2021-41773](https://www.cve.org/CVERecord?id=CVE-2021-41773))**: A path traversal flaw in Apache httpd 2.4.49 that allowed attackers to map URLs to files outside the document root, leading to arbitrary file reads and potential RCE. +2. **Ruby on Rails ([CVE-2019-5418](https://www.cve.org/CVERecord?id=CVE-2019-5418))**: File content disclosure in Action View through crafted HTTP accept headers, potentially exposing secrets and enabling RCE. +3. **`send` npm module ([CVE-2014-6394](https://www.cve.org/CVERecord?id=CVE-2014-6394))**: A classic Node.js ecosystem example where the popular static file serving module was vulnerable to directory traversal. +4. **`serve` npm module ([CVE-2019-5417](https://www.cve.org/CVERecord?id=CVE-2019-5417))**: Path traversal vulnerability in serve version 7.0.1 that allowed attackers to read arbitrary files on the server. +5. **Jenkins ([CVE-2024-23897](https://www.cve.org/CVERecord?id=CVE-2024-23897))**: Arbitrary file read via CLI "@file" argument expansion. While not pure path traversal, it demonstrates how path-based input can lead to unauthorized file access. +6. **Node.js ([CVE-2023-32002](https://www.cve.org/CVERecord?id=CVE-2023-32002))**: Policy bypass via path traversal in Node.js experimental policy feature, allowing module loading restrictions to be circumvented. + +:::tip[Keep Node.js Updated] +The Node.js team actively patches security vulnerabilities. Always keep your Node.js runtime updated to the latest LTS version to benefit from security fixes. Run `node --version` to check your version. If you need to update, check out our guide on [5 ways to install Node.js](/blog/5-ways-to-install-node-js/) which covers version managers like nvm and fnm that make switching and updating versions easy. +::: + +## The Defense: Building a Secure File Server + +Now that we've seen how attackers exploit path traversal vulnerabilities, let's build a secure implementation that blocks all these attack vectors. We'll create a multi-layered defense using modern Node.js APIs, and I'll explain why each layer matters. + +### Step 1: Path Validation and Canonicalization + +We've already seen how to safely resolve user-provided paths at the beginning of the article. Let's take a closer look at that code and explore in more detail why this approach protects us against path traversal attacks. Then we'll apply this utility to our naive image server. + +```js +// safe-resolve.js +import path from 'node:path' +import fs from 'node:fs/promises' + +/** + * Fully decodes URL-encoded input, handling double/triple encoding. + */ +function fullyDecode(input) { + let result = String(input) + // Decode repeatedly until the string stops changing + // Limit iterations to prevent infinite loops on malformed input + for (let i = 0; i < 10; i++) { + try { + const decoded = decodeURIComponent(result) + if (decoded === result) break + result = decoded + } catch { + // decodeURIComponent throws URIError on malformed sequences (e.g., '%', '%zz') + break + } + } + return result +} + +/** + * Safely resolves a user-provided path within a root directory. + * IMPORTANT: root must be pre-resolved with fs.realpath() at startup. + */ +export async function safeResolve(root, userPath) { + // 1. Fully decode any URL-encoded characters (handles double encoding) + const decoded = fullyDecode(userPath) + + // 2. Reject null bytes (used to bypass extension checks) + if (decoded.includes('\0')) { + throw new Error('Null bytes not allowed') + } + + // 3. Reject absolute paths immediately + if (path.isAbsolute(decoded)) { + throw new Error('Absolute paths not allowed') + } + + // 4. Reject Windows drive letters (e.g., C:, D:) + if (/^[a-zA-Z]:/.test(decoded)) { + throw new Error('Drive letters not allowed') + } + + // 5. Reject UNC paths (e.g., \\server\share or //server/share) + if (decoded.startsWith('\\\\') || decoded.startsWith('//')) { + throw new Error('UNC paths not allowed') + } + + // 6. Resolve to canonical path + const safePath = path.resolve(root, decoded) + + // 7. Follow symlinks to get the real path + const realPath = await fs.realpath(safePath) + + // 8. Verify the path stays within root + if (!realPath.startsWith(root + path.sep)) { + throw new Error('Path traversal detected') + } + + return realPath +} +``` + +### Understanding the Security Measures + +Let's break down each security measure in our `safeResolve` function: + +1. **Full Input Decoding**: The `fullyDecode` function handles URL-encoded characters in a loop, decoding repeatedly until the string stops changing. This catches double encoding attacks (`%252F` → `%2F` → `/`) and triple encoding (`%25252F` → `%252F` → `%2F` → `/`). We limit to 10 iterations to prevent denial-of-service (DoS) attacks where an attacker sends deeply nested encoded input to keep the server busy, potentially making it unresponsive or causing it to crash. Note that `decodeURIComponent` throws a `URIError` on malformed sequences like `%` or `%zz`, so we wrap it in a try/catch and stop decoding if an error occurs. + +2. **Null Byte Rejection**: Null bytes (`\0`) are used in null byte injection attacks to truncate paths. For example, `valid.jpg\0../../etc/passwd` might pass extension checks but access different files. We reject these explicitly. + +3. **Absolute Path Rejection**: `path.isAbsolute()` prevents attackers from specifying absolute paths like `/etc/passwd` directly. + +4. **Windows Drive Letter Rejection**: Paths like `C:` or `D:` can escape to different drives on Windows. We reject these with a regex check. + +5. **UNC Path Rejection**: UNC paths (`\\server\share` or `//server/share`) can access network resources. We block these to prevent network-based attacks. + +6. **Path Resolution**: `path.resolve()` normalizes the path, handling `.` and `..` segments correctly. This converts relative paths to absolute paths and removes any path traversal sequences. + +7. **Symlink Resolution**: `fs.realpath()` follows symbolic links to their actual destinations, preventing symlink-based escapes. An attacker could create a symlink inside the uploads directory pointing to sensitive files elsewhere, and this check prevents that attack. + +8. **Boundary Checking**: The `startsWith(root + path.sep)` check verifies the resolved path is still within our allowed directory. Adding `path.sep` prevents a subtle bug where a path like `/uploads-backup/secret.txt` would incorrectly pass a check against `/uploads`. + +:::important[Why Both Resolve and Realpath?] +`path.resolve()` handles `..` sequences but doesn't follow symlinks. `fs.realpath()` follows symlinks and returns the canonical path, but throws `ENOENT` if the file doesn't exist. Using both provides defense in depth: `resolve()` normalizes traversal sequences, while `realpath()` catches symlink-based escapes. If a file doesn't exist, `realpath()` throws before the boundary check, which is the safe behavior (don't reveal whether paths outside root exist). +::: + +### Step 2: Secure Streaming Implementation + +Now let's update our server to use this secure path resolution: + +```js {9,16-21} +// secure-image-server.js +import { createServer } from 'node:http' +import { createReadStream } from 'node:fs' +import { realpath } from 'node:fs/promises' +import path from 'node:path' +import { safeResolve } from './safe-resolve.js' + +// Resolve root at startup to handle symlinks (e.g., /var -> /private/var on macOS) +const ROOT = await realpath(path.resolve(process.cwd(), 'uploads')) + +const server = createServer(async (req, res) => { + const url = new URL(req.url, `http://${req.headers.host}`) + const rel = url.pathname.replace(/^\/images\//, '') + + // SECURE: Use safeResolve to validate and resolve the path + const filePath = await safeResolve(ROOT, rel).catch(() => null) + if (!filePath) { + res.writeHead(400) + res.end('Invalid path') + return + } + + // Set content type based on file extension + const ext = path.extname(filePath).toLowerCase() + const type = + ext === '.jpg' || ext === '.jpeg' + ? 'image/jpeg' + : ext === '.png' + ? 'image/png' + : ext === '.gif' + ? 'image/gif' + : 'application/octet-stream' + + const stream = createReadStream(filePath) + stream.once('open', () => { + res.writeHead(200, { 'Content-Type': type }) + stream.pipe(res) + }) + stream.once('error', () => { + res.writeHead(404) + res.end('Image not found') + }) +}) + +server.listen(3000, () => { + console.log('Secure image server running at http://localhost:3000') +}) +``` + +### Why This Implementation is Secure + +The changes are minimal but effective: + +1. **Path Validation**: The `safeResolve` function validates all user input before any file access occurs, blocking traversal attempts, null bytes, absolute paths, and other attack vectors. +2. **Root Resolved at Startup**: Using `realpath()` on the root directory at startup ensures symlinks are resolved correctly (important on systems like macOS where `/var` is a symlink to `/private/var`). +3. **Early Rejection**: Invalid paths are caught and rejected with a generic error message before reaching any file operations, avoiding information leakage about the filesystem structure. +4. **Minimal Changes**: The rest of the code remains identical to the original, making it easy to understand and audit the security fix. + +:::tip[Want Even More Security?] +For production applications, consider using [`pipeline()`](/blog/reading-writing-files-nodejs/#stream-composition-and-processing) instead of `.pipe()` for better error handling and automatic cleanup. You can also open a file handle immediately after validation to minimize TOCTOU (time-of-check-time-of-use) race conditions. Learn more in our [file operations guide](/blog/reading-writing-files-nodejs/#nodejs-streams-memory-efficient-file-processing). +::: + +## Additional Security Measures + +Our secure implementation handles the core path traversal vulnerability, but security is about layers. In this section, we'll explore additional measures that provide defense in depth, covering edge cases and platform-specific concerns that could otherwise leave gaps in your protection. + +### Defense in Depth + +While our secure implementation addresses the primary vulnerability, security best practice demands multiple layers of protection: + +1. **Input Validation**: Implement strict validation for allowed characters and patterns +2. **Allowlist Approach**: When possible, maintain an allowlist of permitted files +3. **Rate Limiting**: Prevent brute force attempts to discover files +4. **File Permissions**: Run your Node.js process with minimal filesystem permissions +5. **Containerization**: Use containers to limit filesystem access at the OS level + +### Integration with Express.js + +If you're using Express.js, here's how to integrate secure path resolution: + +```js {11} +// express-secure-image-server.js +import express from 'express' +import path from 'node:path' +import { safeResolve } from './safe-resolve.js' + +const app = express() +const ROOT = path.resolve(process.cwd(), 'uploads') + +app.get('/files/:filepath(*)', async (req, res) => { + try { + const safePath = await safeResolve(ROOT, req.params.filepath) + res.sendFile(safePath) + } catch (error) { + console.error('Path validation failed:', error.message) + res.status(400).send('Invalid path') + } +}) + +app.listen(3000) +``` + +:::note +Express's `res.sendFile()` has some built-in protections, but always validate paths yourself rather than relying on framework behavior. +::: + +### Implementing Input Validation + +While `safeResolve` protects against path traversal, adding input validation creates an additional safety net. This follows the principle of defense in depth: if one layer fails (due to a bug, misconfiguration, or a novel attack vector), other layers can still catch the threat. + +Input validation is particularly valuable because it rejects malicious input early, before it reaches more complex logic. This makes your code easier to reason about and debug, and it can also improve performance by avoiding unnecessary filesystem operations on obviously invalid input. + +Here's an example of strict filename validation: + +```js +// validate-filename.js +function validateFileName(fileName) { + // Only allow alphanumeric characters, dots, hyphens, and underscores + const validPattern = /^[a-zA-Z0-9._-]+$/ + + if (!validPattern.test(fileName)) { + throw new Error('Invalid filename') + } + + // Reject files starting with a dot (hidden files) + if (fileName.startsWith('.')) { + throw new Error('Hidden files not allowed') + } + + // Reject files with path separators + if (fileName.includes('/') || fileName.includes('\\')) { + throw new Error('Path separators not allowed') + } + + return fileName +} +``` + +This validation layer catches attacks even before path resolution, providing defense in depth. + +### Windows-Specific Considerations + +If your application runs on Windows (or might be deployed there), you need to account for how Windows handles paths differently from Unix-like systems. These differences aren't just cosmetic; they can create security gaps if your validation logic assumes Unix conventions. + +Windows has unique path characteristics that require additional attention: + +1. **Drive Letters**: Ensure paths can't escape to different drives (`C:\`, `D:\`) +2. **UNC Paths**: Block UNC paths like `\\server\share\file` +3. **Reserved Names**: Avoid Windows reserved names like `CON`, `PRN`, etc. +4. **Case Insensitivity**: Windows treats `File.txt` and `file.txt` as the same + +```js +// windows-path-utils.js +function isWindowsReservedName(name) { + const reservedNames = [ + 'CON', + 'PRN', + 'AUX', + 'NUL', + 'COM0', + 'COM1', + 'COM2', + 'COM3', + 'COM4', + 'COM5', + 'COM6', + 'COM7', + 'COM8', + 'COM9', + 'LPT0', + 'LPT1', + 'LPT2', + 'LPT3', + 'LPT4', + 'LPT5', + 'LPT6', + 'LPT7', + 'LPT8', + 'LPT9', + 'CONIN$', + 'CONOUT$', + ] + + const baseName = path.basename(name, path.extname(name)).toUpperCase() + return reservedNames.includes(baseName) +} + +function isUNCPath(pathStr) { + // UNC paths start with \\ or // + return pathStr.startsWith('\\\\') || pathStr.startsWith('//') +} + +function hasDriveLetter(pathStr) { + // Check for C:, D:, etc. + return /^[a-zA-Z]:/.test(pathStr) +} +``` + +:::warning[Platform-Specific Attacks] +Always test your security measures on the platforms you deploy to. A validator that works on Linux might fail on Windows due to path separator differences (`/` vs `\`) or case sensitivity. +::: + +### Race Condition Protection (TOCTOU) + +**Time-of-Check-Time-of-Use (TOCTOU)** attacks exploit the time gap between validating a path and actually accessing the file. For a deeper dive into race conditions in Node.js, see our [comprehensive guide on Node.js race conditions](/blog/node-js-race-conditions/). During this gap, an attacker might: + +- Replace a safe file with a symlink to a sensitive file +- Swap directories to bypass validation + +Our main server implementation above already mitigates this by opening a file handle immediately after path validation using `fs/promises.open()`. By streaming from the file handle rather than the path, we ensure the file being accessed is the same one that was validated. + +:::note[TOCTOU in Practice] +While TOCTOU attacks are theoretically possible, they're difficult to exploit in practice with our `safeResolve` implementation because: + +1. `realpath()` validates at access time, not just check time +2. The attack window is extremely small (microseconds) +3. The attacker needs write access to the uploads directory + +However, defense in depth means we still minimize the risk by using file handles. +::: + +## Testing Your Implementation + +Writing secure code is only half the battle. You also need to verify that your defenses actually work against the attack vectors we've discussed. In this section, we'll build a test suite that validates our `safeResolve` function against common attack patterns, giving you confidence that your implementation is solid. + +### Security Testing with Assertions + +Security code that isn't tested is security code you can't trust. Unlike functional bugs that cause visible failures, security vulnerabilities often remain silent until exploited. Automated tests ensure your defenses work as expected and catch regressions when code changes. + +A good security test suite should cover both positive cases (valid input works correctly) and negative cases (malicious input is rejected). For path traversal specifically, test against all the attack vectors we've discussed: basic traversal, URL encoding, double encoding, null bytes, and absolute paths. + +Here's an example test suite using the Node.js built-in test runner: + +```js +// safe-resolve.test.js +import { describe, it } from 'node:test' +import assert from 'node:assert' +import { safeResolve } from './safe-resolve.js' + +const root = '/app/uploads' + +describe('safeResolve', () => { + it('should resolve valid paths correctly', async () => { + const result = await safeResolve(root, 'images/cat.jpg') + assert(result.startsWith(root)) + }) + + it('should block basic traversal', async () => { + await assert.rejects( + safeResolve(root, '../../etc/passwd'), + /Path traversal detected/, + ) + }) + + it('should reject absolute paths', async () => { + await assert.rejects( + safeResolve(root, '/etc/passwd'), + /Absolute paths not allowed/, + ) + }) + + it('should block URL-encoded traversal', async () => { + await assert.rejects( + safeResolve(root, '..%2F..%2Fetc%2Fpasswd'), + /Path traversal detected/, + ) + }) + + it('should block double-encoded traversal', async () => { + await assert.rejects( + safeResolve(root, '..%252F..%252Fetc%252Fpasswd'), + /Path traversal detected/, + ) + }) + + it('should reject null bytes', async () => { + await assert.rejects( + safeResolve(root, 'valid.jpg\0../../etc/passwd'), + /Null bytes not allowed/, + ) + }) + + it('should reject UNC paths', async () => { + await assert.rejects( + safeResolve(root, '//server/share/sensitive.txt'), + /UNC paths not allowed|Absolute paths not allowed/, + ) + }) +}) +``` + +Run the tests with `node --test safe-resolve.test.js`. + +### Penetration Testing Checklist + +Automated tests are essential, but they only cover the scenarios you've anticipated. Manual penetration testing helps uncover edge cases and unexpected behaviors that automated tests might miss. Before deploying to production, walk through this checklist manually using tools like `curl` or a browser's developer tools to craft malicious requests. + +Test your implementation against these attack scenarios: + +- [ ] **Basic Traversal**: `../../../etc/passwd` +- [ ] **URL Encoding**: `..%2F..%2Fetc%2Fpasswd` +- [ ] **Double Encoding**: `..%252F..%252Fetc%252Fpasswd` +- [ ] **Unicode/UTF-8**: `..%c0%af..%c0%afetc%c0%afpasswd` +- [ ] **Null Bytes**: `valid.jpg%00../../etc/passwd` +- [ ] **Backslashes**: `..\..\..\windows\system32\config\sam` +- [ ] **Mixed Separators**: `..\/..\/etc/passwd` +- [ ] **Absolute Paths**: `/etc/passwd`, `C:\Windows\System32` +- [ ] **UNC Paths**: `\\server\share\sensitive.txt` +- [ ] **Long Paths**: Extremely long path strings (buffer overflow attempts) +- [ ] **Symlink Attacks**: Create symlink in uploads pointing outside + +:::tip[Automated Security Testing] +Consider using tools like: + +- **[Snyk](https://snyk.io/)** for dependency vulnerability scanning +- **[npm audit](https://docs.npmjs.com/cli/v8/commands/npm-audit)** for known vulnerabilities in dependencies +- **[OWASP ZAP](https://www.zaproxy.org/)** for web application security testing +- **[Burp Suite](https://portswigger.net/burp)** for manual penetration testing + ::: + +## Monitoring and Incident Response + +Even with robust defenses in place, monitoring is essential. Attackers often probe systems before launching full attacks, and detecting these early attempts can help you respond before any damage occurs. + +Comprehensive logging serves two critical purposes. First, it creates an audit trail for investigating incidents after the fact, helping you understand what happened, when, and how. Second, it enables automated mitigation strategies: when you detect suspicious patterns (like repeated traversal attempts from the same IP), you can automatically block the attacker, rate-limit their requests, or trigger alerts for manual review. This proactive approach can stop an attack in progress and prevent escalation. + +### Logging Suspicious Activity + +Here's how to implement security event logging: + +```js +// security-logger.js +import { createWriteStream } from 'node:fs' + +const securityLog = createWriteStream('security.log', { flags: 'a' }) + +function logSecurityEvent(event, details) { + const timestamp = new Date().toISOString() + const logEntry = { + timestamp, + event, + ...details, + } + + securityLog.write(JSON.stringify(logEntry) + '\n') + console.error(`[SECURITY] ${event}:`, details) +} + +// Usage in your server +const server = createServer(async (req, res) => { + try { + const url = new URL(req.url, `http://${req.headers.host}`) + const rel = url.pathname.replace(/^\/images\//, '') + + const imagePath = await safeResolve(ROOT, rel) + // ... serve file + } catch (error) { + logSecurityEvent('path_traversal_attempt', { + path: rel, + decoded: fullyDecode(rel), // Using the fullyDecode helper from earlier + userAgent: req.headers['user-agent'], + ip: req.socket.remoteAddress, + error: error.message, + }) + + // Return generic error to client + if (!res.headersSent) { + res.writeHead(400) + } + res.end('Invalid path') + } +}) +``` + +### Detecting Attack Patterns + +Monitor logs for suspicious patterns that might indicate an attack: + +```js +// detect-attacks.js +const suspiciousPatterns = [ + /\.\./, // Directory traversal + /%2e%2e/i, // Encoded dots + /%252e/i, // Double encoded + /\0/, // Null bytes + /etc\/passwd/, // Common target + /\.env/, // Environment files + /\.ssh/, // SSH keys + /\/\.\./, // Absolute traversal +] + +function isSuspiciousPath(pathStr) { + return suspiciousPatterns.some((pattern) => pattern.test(pathStr)) +} + +// Enhanced logging +if (isSuspiciousPath(rel)) { + logSecurityEvent('high_risk_path_detected', { + path: rel, + ip: req.socket.remoteAddress, + timestamp: Date.now(), + }) +} +``` + +### Incident Response Plan + +If you detect a path traversal attack: + +1. **Immediate Response** + - Block the attacking IP address (temporarily or permanently) + - Review recent logs for the same IP or user agent + - Check if any sensitive files were actually accessed + +2. **Investigation** + - Analyze access logs for patterns and scope + - Check file access timestamps for sensitive files + - Review application logs for other suspicious activities + - Determine if the attack was automated or targeted + +3. **Remediation** + - Patch the vulnerability immediately + - Review and strengthen validation logic + - Update security tests to prevent regression + - Consider implementing Web Application Firewall (WAF) rules + +4. **Post-Incident** + - Document the incident and response + - Update incident response procedures + - Rotate any credentials that might have been exposed + - Notify relevant stakeholders if data was compromised + +## Best Practices Summary + +### Secure Coding Checklist + +- [ ] **Never Trust User Input** - Always validate and sanitize all user-provided data +- [ ] **Use Absolute Paths** - Work with resolved, canonical paths internally +- [ ] **Implement Boundary Checks** - Verify paths stay within allowed directories +- [ ] **Handle Errors Gracefully** - Don't expose internal details to users +- [ ] **Layer Your Defenses** - Multiple validation steps (defense in depth) +- [ ] **Decode Before Validation** - Handle URL encoding, double encoding, etc. +- [ ] **Follow Symlinks** - Use `realpath()` to prevent symlink-based escapes +- [ ] **Log Security Events** - Track suspicious activities for monitoring +- [ ] **Test Thoroughly** - Include security tests in your test suite + +### Node.js Specific Recommendations + +1. **Use Modern APIs**: Prefer `fs/promises` over callbacks for cleaner async code +2. **Stream Large Files**: Use [streams for memory efficiency](/blog/reading-writing-files-nodejs/#nodejs-streams-memory-efficient-file-processing) +3. **Handle Backpressure**: Use `pipeline()` for proper stream management +4. **Validate Early**: Check paths before any file system operation +5. **Consider Security Modules**: Use packages like `helmet` for HTTP security headers + +### Operational Security + +1. **Principle of Least Privilege**: Run your application with minimal filesystem permissions +2. **Regular Updates**: Keep Node.js and dependencies updated with security patches +3. **Security Audits**: Regularly audit code and dependencies (`npm audit`) +4. **Comprehensive Monitoring**: Implement logging and alerting for security events +5. **Incident Response Plan**: Have procedures ready for responding to security incidents +6. **Container Isolation**: Use Docker or similar to limit filesystem access at OS level + +## Conclusion: Security is Not Optional + +Path traversal vulnerabilities are deceptively simple to introduce but can have devastating consequences. A single missing validation can expose your entire filesystem to attackers. + +**Key takeaways:** + +1. **Never trust user input** - Validate, decode, and sanitize all user-provided paths +2. **Use canonical paths** - Resolve symlinks and normalize paths with `path.resolve()` and `fs.realpath()` +3. **Implement boundary checks** - Verify resolved paths stay within allowed directories +4. **Handle errors securely** - Don't leak internal details; log them server-side instead +5. **Layer your defenses** - Multiple validation steps provide protection even if one fails +6. **Test thoroughly** - Include security tests alongside functional tests + +By incorporating these practices into your development workflow, you'll build Node.js applications that can withstand common attack vectors. Security isn't an afterthought; it's a fundamental aspect of writing reliable, professional code. + +:::tip[Continue Learning] +This article builds on concepts from our [Node.js File Operations Guide](/blog/reading-writing-files-nodejs/). If you haven't read it yet, check it out to deepen your understanding of Node.js file handling with promises, streams, and file handles. +::: + +## Additional Resources + +### Essential Reading + +1. **[OWASP Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal)** - Comprehensive overview from OWASP +2. **[CWE-22: Improper Limitation of a Pathname](https://cwe.mitre.org/data/definitions/22.html)** - Common Weakness Enumeration entry +3. **[Node.js Security Best Practices](https://nodejs.org/en/learn/getting-started/security-best-practices)** - Official Node.js security guide +4. **[SANS Top 25 Software Errors](https://www.sans.org/top25-software-errors/)** - Industry-standard security issues + +### Tools and Libraries + +- **[@sindresorhus/is-path-inside](https://github.com/sindresorhus/is-path-inside)** - Utility to check if a path is inside another path +- **[path-type](https://github.com/sindresorhus/path-type)** - Check what a path is (file, directory, symlink) +- **[helmet](https://helmetjs.github.io/)** - Security HTTP headers for Express.js + +### Further Learning + +For a deeper dive into Node.js security, consider: + +- **Node.js Design Patterns** - Our book covers security patterns and best practices throughout ([learn more](/)) +- **[Liran Tal's Node.js Security](https://www.nodejs-security.com/)** - Comprehensive Node.js security resources +- **[Snyk's Node.js Security Guide](https://snyk.io/blog/nodejs-security-best-practices/)** - Modern security practices + +Remember: **security is an ongoing process**, not a one-time fix. Stay informed about new vulnerabilities, regularly review your code, and always prioritize secure coding practices from the start. diff --git a/src/content/blog/reading-writing-files-nodejs/index.md b/src/content/blog/reading-writing-files-nodejs/index.md index f4bc835..e8b6bd7 100644 --- a/src/content/blog/reading-writing-files-nodejs/index.md +++ b/src/content/blog/reading-writing-files-nodejs/index.md @@ -92,6 +92,12 @@ Notice we're using `try/catch` here as well, because file write operations can f - **ENOTDIR**: Part of the path is not a directory ::: +:::warning[Security: Validating User-Provided File Paths] +When your application handles user-provided file paths (e.g., serving uploaded files or processing user-specified paths), you must validate them to prevent **path traversal attacks**. Attackers can use sequences like `../` to escape your intended directory and access sensitive files like `/etc/passwd` or application credentials. + +Never pass user input directly to `readFile()`, `writeFile()`, or similar functions without validation. Learn how to properly validate paths and build secure file servers in our comprehensive guide on [preventing path traversal attacks in Node.js](/blog/nodejs-path-traversal-security/). +::: + ### Reading and Writing Binary Files So far, we've been working with text files, but what happens when you need to handle images, videos, or audio files? Not all files are text-based, and Node.js handles binary data just as elegantly. Here's a more advanced example showing how to work with binary data by creating and reading WAV audio files: diff --git a/src/lib/analytics.ts b/src/lib/analytics.ts index 36bf800..9f34c06 100644 --- a/src/lib/analytics.ts +++ b/src/lib/analytics.ts @@ -155,6 +155,18 @@ export type AnalyticsEventName = // Debug Mode // ============================================================================ +/** + * Check if running on localhost (development environment) + */ +export function isLocalhost(): boolean { + if (typeof window === 'undefined') return false + + return ( + window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' + ) +} + /** * Check if analytics debug mode is enabled. * Enable via URL param: ?debug_analytics=true @@ -165,11 +177,8 @@ export function isDebugMode(): boolean { const urlParams = new URLSearchParams(window.location.search) const debugParam = urlParams.get('debug_analytics') === 'true' - const isDev = - window.location.hostname === 'localhost' || - window.location.hostname === '127.0.0.1' - return debugParam || isDev + return debugParam || isLocalhost() } /** @@ -206,9 +215,19 @@ function getGtag(): ((...args: unknown[]) => void) | null { return window.gtag ?? null } +/** + * Check if debug analytics param is enabled via URL + */ +function hasDebugParam(): boolean { + if (typeof window === 'undefined') return false + const urlParams = new URLSearchParams(window.location.search) + return urlParams.get('debug_analytics') === 'true' +} + /** * Generic event tracking function * Uses the global gtag() function directly for event tracking. + * Events are not sent when running on localhost (unless ?debug_analytics=true is set). */ export function trackEvent>( eventName: string, @@ -216,6 +235,11 @@ export function trackEvent>( ): void { debugLog(eventName, params) + // Skip sending events on localhost unless debug param is set + if (isLocalhost() && !hasDebugParam()) { + return + } + const gtag = getGtag() if (gtag) { gtag('event', eventName, params)