I've been working on creating an interactive tutorial for Perl, since places like Codecademy refuse to believe that a lot of system administration is still performed via Perl and there aren't many resources that teach basic Perl via an interactive shell.
To do this I had to figure out a way to secure the environment from potentially malicious code that a user could write and execute. Docker was the solution I went with — spin up a container, run the code inside it, return the output. The container gets trashed after each run, so even if someone submits something nasty it can't touch the host.
The Docker image
Start with a Perl base image and install whatever modules your tutorials require:
FROM perl:5.38-slim
# Install any modules needed for your tutorials
RUN cpanm --notest \
Data::Dumper \
List::Util \
Scalar::Util \
POSIX
# Create a non-root user to run submitted code
RUN useradd -m -u 1000 sandbox
USER sandbox
WORKDIR /home/sandbox
Build and tag it:
docker build -t perl-sandbox .
Locking it down
The key to making this safe is combining several Docker isolation flags when you run the container:
docker run \
--rm \ # destroy container after exit
--network none \ # no network access
--memory 64m \ # cap memory
--memory-swap 64m \ # no swap either
--cpus 0.5 \ # limit CPU
--read-only \ # read-only filesystem
--tmpfs /tmp:size=10m \ # writable scratch space only in /tmp
--pids-limit 64 \ # prevent fork bombs
--security-opt no-new-privileges \
perl-sandbox \
perl -e "$USER_CODE"
The combination of --network none and --read-only is the most important part. No outbound connections and no ability to write to the filesystem means even a successful escape attempt has nowhere to go.
Passing code in via PHP
I went with PHP for the web layer because Perl CGI is painful and this was small enough not to warrant a full Perl web framework:
<?php
$code = escapeshellarg($_POST['code']);
$output = shell_exec(
"docker run --rm --network none --memory 64m --cpus 0.5 " .
"--read-only --tmpfs /tmp:size=10m --pids-limit 64 " .
"--security-opt no-new-privileges perl-sandbox " .
"perl -e $code 2>&1"
);
echo htmlentities($output);
?>
Timeouts
One thing the above doesn't handle: infinite loops. Add a timeout wrapper so a while(1){} doesn't hang your server:
<?php
$code = escapeshellarg($_POST['code']);
$output = shell_exec(
"timeout 5 docker run --rm --network none --memory 64m --cpus 0.5 " .
"--read-only --tmpfs /tmp:size=10m --pids-limit 64 " .
"--security-opt no-new-privileges perl-sandbox " .
"perl -e $code 2>&1"
);
if ($output === null) {
$output = "Execution timed out after 5 seconds.";
}
echo htmlentities($output);
?>
So far so good — nobody has managed to escape the container and the host has stayed clean. If you're building something similar for Python, Ruby, or any other interpreted language the same pattern applies; just swap the base image and the interpreter.
Tell your friends...
