Skip to content

Commit a257c83

Browse files
committed
chore: improvements on path traversal article
1 parent 4010da1 commit a257c83

1 file changed

Lines changed: 85 additions & 0 deletions

File tree

  • src/content/blog/nodejs-path-traversal-security

src/content/blog/nodejs-path-traversal-security/index.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,91 @@ While TOCTOU attacks are theoretically possible, they're difficult to exploit in
576576
However, defense in depth means we still minimize the risk by using file handles.
577577
:::
578578

579+
### Infrastructure-Level Hardening
580+
581+
So far, we've focused on securing your Node.js code itself. But what if an attacker finds a vulnerability you haven't anticipated? What if there's a bug in a dependency? Infrastructure-level protections act as a safety net, limiting the damage even when application-level defenses fail.
582+
583+
Think of it this way: secure code is your first line of defense, but infrastructure hardening ensures that a breach doesn't become a catastrophe. Here are several strategies to consider:
584+
585+
#### Run with Minimal Permissions
586+
587+
One of the simplest and most effective mitigations is running your Node.js process with a dedicated user account that has only the permissions it absolutely needs:
588+
589+
```bash
590+
# Create a dedicated user for the application
591+
useradd --system --no-create-home --shell /bin/false nodeapp
592+
593+
# Set ownership of application files
594+
chown -R nodeapp:nodeapp /app
595+
596+
# Set restrictive permissions on the uploads directory
597+
chmod 750 /app/uploads
598+
599+
# Run the application as the dedicated user
600+
su -s /bin/bash -c 'node server.js' nodeapp
601+
```
602+
603+
With this setup, even if an attacker exploits a path traversal vulnerability, they can only access files that the `nodeapp` user has permission to read. System files like `/etc/shadow` or other users' home directories remain inaccessible.
604+
605+
#### Node.js Permission Model
606+
607+
Node.js has a built-in [permission model](https://nodejs.org/api/permissions.html) (stable since Node.js 22) that restricts access to system resources at the runtime level. When you start Node.js with the `--permission` flag, all resource access is denied by default unless explicitly allowed.
608+
609+
The `--allow-fs-read` flag is particularly relevant for path traversal protection. It lets you specify exactly which paths your application can read:
610+
611+
```bash
612+
# Only allow reading from the uploads directory and node_modules
613+
node --permission \
614+
--allow-fs-read=/app/uploads/ \
615+
--allow-fs-read=/app/node_modules/ \
616+
server.js
617+
```
618+
619+
With this configuration, even if a path traversal attack bypasses your application-level validation, Node.js itself will block any attempt to read files outside the allowed paths, throwing an `ERR_ACCESS_DENIED` error.
620+
621+
You can also check permissions at runtime using `process.permission.has()`:
622+
623+
```js
624+
if (!process.permission.has('fs.read', '/etc/passwd')) {
625+
console.log('Cannot read /etc/passwd - permission denied at runtime level')
626+
}
627+
```
628+
629+
:::note[Permission Model Limitations]
630+
The Node.js permission model is designed as a "seat belt" for trusted code, not as a sandbox against malicious code. It won't protect against attacks that exploit native addons or existing file descriptors. Use it as one layer in your defense-in-depth strategy, not as your only protection.
631+
:::
632+
633+
#### Containerization with Docker
634+
635+
Docker provides excellent sandboxing by isolating your application in a container with its own filesystem view. The application can have broad access within the container, but the container itself has limited access to the host system. Even if an attacker escapes your uploads directory through a path traversal vulnerability, they're still trapped inside the container with no access to the host filesystem.
636+
637+
For maximum security, run your container with a non-root user, drop all Linux capabilities with `--cap-drop=ALL`, and use `--security-opt=no-new-privileges` to prevent privilege escalation.
638+
639+
#### Other Sandboxing Strategies
640+
641+
Beyond Docker, several other sandboxing approaches can limit the blast radius of a successful attack:
642+
643+
- **chroot jails**: The classic Unix approach to restricting filesystem access. The application sees a limited directory tree as its entire filesystem. While not as robust as containers, it's a lightweight option for simple deployments.
644+
645+
- **systemd service hardening**: If running as a systemd service, use directives like `ProtectSystem=strict`, `ProtectHome=true`, `PrivateTmp=true`, and `ReadOnlyPaths=/` to restrict filesystem access.
646+
647+
- **SELinux/AppArmor profiles**: These Linux Security Modules provide mandatory access control. Create a profile that explicitly lists which files and directories your application can access, denying everything else by default.
648+
649+
- **seccomp filters**: Restrict which system calls your application can make. Node.js needs a relatively small set of syscalls, and blocking dangerous ones like `ptrace` or `mount` adds another layer of protection.
650+
651+
#### Web Application Firewall (WAF)
652+
653+
A WAF can detect and block path traversal attempts before they even reach your application:
654+
655+
- **Cloud-based WAFs** (AWS WAF, Cloudflare, Akamai) provide managed rule sets that detect common attack patterns including path traversal
656+
- **Self-hosted options** like ModSecurity with the OWASP Core Rule Set can be deployed in front of your Node.js application
657+
658+
WAFs are particularly valuable because they protect against attacks targeting vulnerabilities you might not even know exist in your code or dependencies.
659+
660+
:::tip[Defense in Depth in Practice]
661+
The most resilient systems combine multiple strategies. A well-protected deployment might include: validated code paths (application layer) + Docker container (isolation) + non-root user (least privilege) + read-only mounts (filesystem protection) + WAF (network perimeter). Each layer reduces risk independently, so even if one fails, others remain.
662+
:::
663+
579664
## Testing Your Implementation
580665

581666
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.

0 commit comments

Comments
 (0)