skip to Main Content

I’m making a python script, that wraps call to docker run by creating a child process with pexpect (pexpect is essential to me, because command might be interactive):

import argparse
import pexpect


parser = argparse.ArgumentParser()
parser.add_argument("some_positional_argument")
parser.add_argument("command", nargs=argparse.REMAINDER)

args = parser.parse_args()

command = " ".join(args.command)
docker_run = f"docker run -it --rm ubuntu {command}"
print(f"docker_run: {docker_run}")

child = pexpect.spawn(docker_run)
child.interact()
child.close()
exit(child.exitstatus)

Problem is, when passing bash command with quotes to the command argument, python will interpret it as just a string value, so quotes will be gone:

python3 t.py foo bash -c 'echo hi'
docker_run: docker run -it --rm ubuntu bash -c echo hi
    # empty line here, because 'bash -c echo' was actually called inside the container

What is interesting to me, how docker solves this issue internally? Because docker run is similar CLI program, that will receive same unquoted echo hi. Example

docker run --rm -it ubuntu bash -c 'echo hi'
hi

works just fine.

I tried to look into docker cli source code:

parse function seemed promising:

but i didn’t find any post processing (like adding quotes back) of the arguments.

2

Answers


  1. pexpect.spawn() has a form where it takes a command and a list of arguments. Use this. Never construct a command by concatenating strings together.

    docker_args = ['run', '-it', '--rm', 'ubuntu'] + args.command
    child = pexpect.spawn('docker', docker_args)
    

    In your example, bash -c 'echo hi' is three shell words, bash, -c, and echo hi; the host shell will remove the single quotes before your program sees it (try printing out args.command). Staying in the list-of-arguments form means you don’t need to worry about what kind of quotes or escaping you might need to implement: you have four docker arguments plus the three-argument command and pass seven arguments to docker.

    String manipulation here opens you up to a shell injection attack. What happens if the command includes "special" shell characters, or single quotes?

    ./t.py foo 'word' ; echo bar > file
    

    pexpect documents that it won’t interpret this punctuation or run a shell at all, so you’ll get potentially confusing results. Of note subprocess.call(shell=True) will run a shell, and this would cause your program to run that echo command after the program exits. Using the list syntax avoids this ambiguity: you have a list item containing ; that is passed as an argument to the command and never interpreted as anything else.

    Also consider whether the Docker SDK for Python can meet your needs. client.containers.run() takes Python-native keyword arguments similar to docker run, but the corresponding container.attach_socket() method to interact with the container’s stdin/stdout can be trickier to use.

    Login or Signup to reply.
  2. Python has a utility to give shell-like quote handling, shlex, https://pymotw.com/2/shlex/ (or officil python docs)

    So replace

    " ".join(args.command) 
    

    with

    shlex.join(args.command)
    

    to get properly quoted text.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search