Skip to content

Commit a61c3af

Browse files
authored
Add bandit security linter and refactor the implementation of --sandbox (#176)
1 parent f9db514 commit a61c3af

File tree

5 files changed

+77
-72
lines changed

5 files changed

+77
-72
lines changed

.pre-commit-config.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ repos:
4545
"-sn", # Don't display the score
4646
"--rcfile=pylintrc"
4747
]
48+
# run the bandit security linter
49+
- repo: https://github.com/PyCQA/bandit
50+
rev: 1.7.10
51+
hooks:
52+
- id: bandit
53+
args: [-c, bandit.yaml]
4854
# Type check the Python code with MyPy
4955
- repo: https://github.com/pre-commit/mirrors-mypy
5056
rev: 'v1.11.2'

bandit.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
# This is the configuration file for the bandit python static analysis tool
3+
exclude_dirs:
4+
- build
5+
# We are less worried about tests, as they are not a part of the library meant to be used by users
6+
# with untrusted inputs.
7+
- test
8+
skips:
9+
- B101 # allow the use of assert

src/pdl/pdl.py

+5-39
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import argparse
22
import json
33
import logging
4-
import os
5-
import subprocess
64
import sys
75
from pathlib import Path
86
from typing import Any, Literal, Optional, TypedDict
@@ -23,6 +21,7 @@
2321
)
2422
from .pdl_interpreter import InterpreterState, process_prog
2523
from .pdl_parser import parse_file, parse_str
24+
from .pdl_runner import exec_docker
2625

2726
logger = logging.getLogger(__name__)
2827

@@ -231,43 +230,10 @@ def main():
231230
return
232231

233232
if args.sandbox:
234-
watsonx_apikey = "WATSONX_APIKEY=" + os.environ["WATSONX_APIKEY"]
235-
watsonx_url = "WATSONX_URL=" + os.environ["WATSONX_URL"]
236-
watsonx_project_id = "WATSONX_PROJECT_ID=" + os.environ["WATSONX_PROJECT_ID"]
237-
replicate_api_token = "REPLICATE_API_TOKEN=" + os.environ["REPLICATE_API_TOKEN"]
238-
239-
local_dir = os.getcwd() + ":/local"
240-
try:
241-
args = sys.argv[1:]
242-
args.remove("--sandbox")
243-
subprocess.run(
244-
[
245-
"docker",
246-
"run",
247-
"-v",
248-
local_dir,
249-
"-w",
250-
"/local",
251-
"-e",
252-
watsonx_apikey,
253-
"-e",
254-
watsonx_url,
255-
"-e",
256-
watsonx_project_id,
257-
"-e",
258-
replicate_api_token,
259-
"--rm",
260-
"-it",
261-
"quay.io/project_pdl/pdl",
262-
*args,
263-
],
264-
check=True,
265-
)
266-
except Exception:
267-
print(
268-
"An error occured while running docker. Is the docker daemon running?"
269-
)
270-
return
233+
args = sys.argv[1:]
234+
args.remove("--sandbox")
235+
exec_docker(*args)
236+
assert False # unreachable: exec_docker terminate the execution
271237

272238
initial_scope = {}
273239
if args.data_file is not None:

src/pdl/pdl_interpreter.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33
import re
44
import shlex
5-
import subprocess
5+
import subprocess # nosec
66
import sys
77
import types
88

@@ -926,7 +926,9 @@ def process_expr(scope: ScopeType, expr: Any, loc: LocationType) -> Any:
926926
try:
927927
if expr.startswith(EXPR_START_STRING) and expr.endswith(EXPR_END_STRING):
928928
# `expr` might be a single expression and should not be stringify
929-
env = Environment(
929+
env = Environment( # nosec B701
930+
# [B701:jinja2_autoescape_false] By default, jinja2 sets autoescape to False. Consider using autoescape=True or use the select_autoescape function to mitigate XSS vulnerabilities.
931+
# This is safe because autoescape is not needed since we do not generate HTML
930932
block_start_string="{%%%%%PDL%%%%%%%%%%",
931933
block_end_string="%%%%%PDL%%%%%%%%%%}",
932934
variable_start_string=EXPR_START_STRING,
@@ -1276,14 +1278,20 @@ def step_call_code(
12761278

12771279
def call_python(code: str, scope: dict) -> Any:
12781280
my_namespace = types.SimpleNamespace(PDL_SESSION=__PDL_SESSION, **scope)
1279-
exec(code, my_namespace.__dict__)
1281+
exec(code, my_namespace.__dict__) # nosec B102
1282+
# [B102:exec_used] Use of exec detected.
1283+
# This is the code that the user asked to execute. It can be executed in a docker container with the option `--sandbox`
12801284
result = my_namespace.result
12811285
return result
12821286

12831287

12841288
def call_command(code: str) -> str:
12851289
args = shlex.split(code)
1286-
p = subprocess.run(args, capture_output=True, text=True, check=False)
1290+
p = subprocess.run(
1291+
args, capture_output=True, text=True, check=False, shell=False
1292+
) # nosec B603
1293+
# [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
1294+
# This is the code that the user asked to execute. It can be executed in a docker container with the option `--sandbox`
12871295
if p.stderr != "":
12881296
print(p.stderr, file=sys.stderr)
12891297
if p.returncode != 0:

src/pdl/pdl_runner.py

+45-29
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,52 @@
11
import os
2-
import subprocess
2+
import subprocess # nosec
33
import sys
4+
from shutil import which
45

5-
WATSONX_APIKEY = "WATSONX_APIKEY=" + os.environ["WATSONX_APIKEY"]
6-
WATSONX_URL = "WATSONX_URL=" + os.environ["WATSONX_URL"]
7-
WATSONX_PROJECT_ID = "WATSONX_PROJECT_ID=" + os.environ["WATSONX_PROJECT_ID"]
8-
LOCAL_DIR = os.getcwd() + ":/local"
96

7+
def exec_docker(*args):
8+
try:
9+
docker = which("docker")
10+
if docker is None:
11+
print("Error: unable to find the docker command", file=sys.stderr)
12+
sys.exit(1)
1013

11-
def main():
12-
subprocess.run(
13-
[
14-
"docker",
15-
"run",
16-
"-v",
17-
LOCAL_DIR,
18-
"-w",
19-
"/local",
20-
"-e",
21-
WATSONX_APIKEY,
22-
"-e",
23-
WATSONX_URL,
24-
"-e",
25-
WATSONX_PROJECT_ID,
26-
"--rm",
27-
"-it",
28-
"pdl-runner",
29-
*sys.argv[1:],
30-
],
31-
check=True,
32-
)
14+
watsonx_apikey = "WATSONX_APIKEY=" + os.environ["WATSONX_APIKEY"]
15+
watsonx_url = "WATSONX_URL=" + os.environ["WATSONX_URL"]
16+
watsonx_project_id = "WATSONX_PROJECT_ID=" + os.environ["WATSONX_PROJECT_ID"]
17+
replicate_api_token = "REPLICATE_API_TOKEN=" + os.environ["REPLICATE_API_TOKEN"]
3318

19+
local_dir = os.getcwd() + ":/local"
3420

35-
if __name__ == "__main__":
36-
main()
21+
subprocess.run( # nosec B603
22+
# [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
23+
# This is safe since the environment variables are the arguments of the options `-e` and `*args` is explicitly given by the user
24+
[
25+
docker,
26+
"run",
27+
"-v",
28+
local_dir,
29+
"-w",
30+
"/local",
31+
"-e",
32+
watsonx_apikey,
33+
"-e",
34+
watsonx_url,
35+
"-e",
36+
watsonx_project_id,
37+
"-e",
38+
replicate_api_token,
39+
"--rm",
40+
"-it",
41+
"quay.io/project_pdl/pdl",
42+
*args,
43+
],
44+
check=True,
45+
)
46+
sys.exit(0)
47+
except Exception:
48+
print(
49+
"An error occurred while running docker.",
50+
file=sys.stderr,
51+
)
52+
sys.exit(1)

0 commit comments

Comments
 (0)