Command Injection in Hapi
How Command Injection Manifests in Hapi
Command injection in Hapi applications typically occurs when user input is passed directly to child processes without proper sanitization. The most common scenarios involve using Node.js's child_process module within route handlers to execute system commands, often for legitimate purposes like file operations, system monitoring, or external tool integration.
Consider this vulnerable Hapi route pattern:
const { exec } = require('child_process');
const routes = [
{
method: 'GET',
path: '/run-backup',
handler: (request, h) => {
const backupName = request.query.name || 'default';
const command = `tar -czf /backups/${backupName}.tar.gz /data`;
exec(command, (error, stdout, stderr) => {
if (error) {
return h.response({ error: error.message }).code(500);
}
return h.response({ message: 'Backup created' });
});
}
}
];The vulnerability here is subtle: an attacker can inject additional commands by manipulating the name parameter. For example, requesting /run-backup?name=default.tar.gz;rm -rf / would execute both the backup command and the destructive rm command.
Another common pattern involves using execSync for synchronous operations:
const { execSync } = require('child_process');
const routes = [
{
method: 'POST',
path: '/execute-query',
handler: (request, h) => {
const sql = request.payload.query;
const output = execSync(`sqlite3 database.db "${sql}"`);
return h.response({ result: output.toString() });
}
}
];Here, SQL injection combines with command injection, allowing attackers to execute arbitrary shell commands through the SQLite interface.
Hapi's plugin system can also introduce command injection risks when plugins execute system commands based on configuration or runtime data. Plugins that wrap external tools or services are particularly vulnerable if they don't properly validate input.
Hapi-Specific Detection
Detecting command injection in Hapi applications requires both static code analysis and runtime scanning. Static analysis involves searching for patterns where child_process methods are called with dynamic input.
Using middleBrick's API security scanner, you can identify command injection vulnerabilities by scanning your Hapi application's endpoints. The scanner tests for command injection by attempting to execute harmless commands through input parameters and observing the responses.
Here's how to scan a Hapi application with middleBrick:
npm install -g middlebrick
# Scan a running Hapi server
middlebrick scan http://localhost:3000
# Scan with specific checks focused on injection vulnerabilities
middlebrick scan http://localhost:3000 --checks=command-injection,input-validationThe scanner tests common injection patterns like:
- Semicolon injection (
;) to chain commands - Command substitution (
$(command)) to execute nested commands - Background execution (
&) to run commands asynchronously - Redirection operators (
>,<) to manipulate file operations - Logical operators (
&&,||) to conditionally execute commands
For development workflows, integrate middleBrick into your Hapi project's testing pipeline:
// package.json
{
"scripts": {
"test": "jest",
"security-scan": "middlebrick scan http://localhost:3000 --format=json > security-report.json"
}
}middleBrick's LLM security features are particularly relevant if your Hapi application integrates with AI services. The scanner can detect if your application exposes unauthenticated endpoints that could be used to query AI models, potentially leading to data exfiltration or prompt injection attacks.
Hapi-Specific Remediation
Remediating command injection in Hapi applications involves eliminating the use of shell command execution where possible and implementing strict input validation when it's necessary.
The safest approach is to avoid child_process entirely by using native Node.js libraries. For file operations that might otherwise use shell commands:
const fs = require('fs').promises;
const path = require('path');
const routes = [
{
method: 'GET',
path: '/create-archive',
handler: async (request, h) => {
const backupName = request.query.name || 'default';
const sanitizedName = path.basename(backupName.replace(/[^a-zA-Z0-9-_]/g, ''));
// Use Node.js APIs instead of shell commands
const sourceDir = '/data';
const targetPath = path.join('/backups', `${sanitizedName}.tar.gz`);
// Implement archiving using a library instead of exec
const tar = require('tar');
await tar.c({
gzip: true,
file: targetPath
}, [sourceDir]);
return h.response({ message: 'Backup created' });
}
}
];When shell commands are unavoidable, use the spawn or execFile variants that don't invoke a shell:
const { execFile } = require('child_process');
const routes = [
{
method: 'GET',
path: '/ping',
handler: (request, h) => {
const host = request.query.host || '8.8.8.8';
// Validate input - only allow valid IP addresses or hostnames
if (!/^([a-zA-Z0-9-]+.){1,4}[a-zA-Z]{2,}|\d{1,3}(\.\d{1,3}){3}$/.test(host)) {
return h.response({ error: 'Invalid host format' }).code(400);
}
execFile('ping', ['-c', '4', host], (error, stdout, stderr) => {
if (error) {
return h.response({ error: error.message }).code(500);
}
return h.response({ result: stdout.toString() });
});
}
}
];Implement comprehensive input validation using Hapi's built-in validation:
const Hapi = require('@hapi/hapi');
const Joi = require('@hapi/joi');
const init = async () => {
const server = Hapi.server({ port: 3000 });
const routes = [
{
method: 'POST',
path: '/execute',
options: {
validate: {
payload: Joi.object({
command: Joi.string().pattern(/^[a-zA-Z0-9_-]+$/).required(),
args: Joi.array().items(Joi.string().pattern(/^[a-zA-Z0-9/_.-]*$/))
}).options({ abortEarly: false })
}
},
handler: (request, h) => {
const { command, args } = request.payload;
// Only allow specific, safe commands
const allowedCommands = ['ls', 'pwd', 'whoami'];
if (!allowedCommands.includes(command)) {
return h.response({ error: 'Command not allowed' }).code(403);
}
const { execFile } = require('child_process');
execFile(command, args, (error, stdout, stderr) => {
if (error) {
return h.response({ error: error.message }).code(500);
}
return h.response({ result: stdout.toString() });
});
}
}
];
server.route(routes);
await server.start();
console.log('Server running on %s', server.info.uri);
};
init();
For Hapi plugins that might execute commands, implement sandboxing and resource limits:
const plugin = {
name: 'system-tools',
version: '1.0.0',
register: async (server, options) => {
server.ext('onPreHandler', (request, h) => {
// Sanitize all query parameters
for (const [key, value] of Object.entries(request.query)) {
if (typeof value === 'string') {
request.query[key] = value.replace(/[;|&$`\x22\x27\x3c\x3e\x28\x29]/g, '');
}
}
return h.continue;
});
}
};
module.exports = plugin;
Related CWEs: inputValidation
| CWE ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |
Frequently Asked Questions
How can I test if my Hapi application is vulnerable to command injection?
;ls -la, $(whoami), or && cat /etc/passwd. If the application responds with command output or error messages, it's vulnerable. Use middleBrick's automated scanner to systematically test all endpoints without manual effort.What's the difference between command injection and SQL injection in Hapi applications?
ls, rm, or ping. SQL injection targets database queries. In Hapi, command injection often occurs when using child_process with user input, while SQL injection happens with database query construction. Both can be equally dangerous, but command injection typically provides broader system access.