11

I have a Rust-app executing Python-scripts using PyO3. The Python-scripts are uploaded by users, so I need to check for unsafe code before executing it. The scripts should only be able to do mathematical operations and in no case should access files, operating system resources, or use network functionalities.

To make sure of this I have blacklisted specific keywords like import, open, os, read, write,... If the user is not able to import his own libraries it'd be pretty hard already to execute e.g network functions.

But these blacklisted words should only be checked on lines not commented. E.g:

import something

def func(data):
    return 0

Is disallowed, but

#import something

def func(data):
    return 0

Should be allowed. My current approach is a Regex deleting all the lines who start with a comment before checking the string for blacklisted keywords.

    // We execute arbitrary Python scripts. Even if this is supposed to be a internal
    // application we need to secure this somehow. We blacklist specific Python keywords like
    // "import", "exec", "os",... so no dangerous code can be executed.
    // We import popular mathematical libraries so those can be used.
    fn arbitrary_code_is_secure(&self, code: &String) -> bool {
        let blacklist = vec![
            "import",
            "open",
            "read",
            "write",
            "file",
            "os",
            "exec",
            "eval",
            "socket",
            "http",
            "requests",
            "urllib",
            "sys",
            "traceback",
        ];

        let re_find_commented_lines = Regex::new(r"^#.*").unwrap();
        let code_without_comments = re_find_commented_lines.replace_all(code, "");

        for keyword in blacklist {
            match code_without_comments.find(keyword) {
                Some(_) => { return false; },
                None => (),
            }
        }

        true
    }

Is this approach somewhat secure? And especially: Could the user manipulate the check for commented lines to import custom libraries anyway?

2
  • 2
    What OS does the rust code run under? If it’s Linux, then cgroup namespaces for the python process are a much better solution
    – Josh
    Commented Jan 30 at 13:56
  • Is this for a web application? If so, if you want to preserve the Python syntax, Skulpt may be a nice option.
    – LeoDog896
    Commented Jan 31 at 13:30

3 Answers 3

72

This is not a secure approach for several reasons.

First, using a simplistic search-and-replace approach instead of a Python parser means there's a huge risk that an attacker can take advantage of Python syntax constructs which your validator does not cover. For example, the following code will pass the filter as "secure" despite using eval:

'''
#'''; eval('print(1)')

Your validator misinterprets the second line as a comment. In reality, the line belongs to a multiline string containing # as a character, followed by an eval() method call. Note that I've assumed your regex to be in multiline mode ((?m)^#.*); otherwise, it only matches a single comment at the beginning, which doesn't make sense and conflicts with the replace_all call.

Secondly, trying to blacklist all potentially dangerous constructs means there'll be a never-ending cat-and-mouse game between you and the attackers: While you try to make the list more and more complete, attackers will come up with more and more clever ways for using constructs you haven't though of yet.

Third, you're essentially trying to replicate access control features of the operating system at a purely syntactical level. This is far more fragile than actual OS-level access control. If the scripts mustn't be able to access the file system, perform network operations etc., then the OS can and should in fact enforce that.

A more secure approach should include (at least) the following measures.

  • For validation, use an actual Python parser which understands the code exactly as the Python interpreter that will run the code.
  • Use a whitelist of known harmless language features, not a blacklist of potentially dangerous ones. If only mathematical operations are allowed, it should be possible to define a very limited subset of Python.
  • Alternatively, consider using a domain-specific language with a Python-like syntax instead of Python itself. If you only want to support a few language features, it might be easier to implement your own parser and interpreter rather than reducing Python to only those features.
  • Enforce the restrictions (no file access etc.) at the OS-level, e.g., by using seccomp or SELinux.
  • Run the code in a sandbox which is isolated from the rest of the system. For example, a virtual machine locked down with sVirt, or a security-focused container implementation like gVisor can act as a last resort when all other measures fail.
  • Set resource and time limits to prevent the script from slowing down the system due to a bug or a denial-of-service attack.

In any case, running untrusted code is inherently dangerous and requires a very solid security infrastructure with multiple levels of protection. Validation alone is not enough.

12
  • 18
    This is a good answer. To emphasize the whitelist point, this construct isn't being validated against: __builtins__.__dict__['eval']('print(1)'). I'm sure there are many more.... Commented Jan 29 at 12:41
  • 28
    "never-ending cat-and-mouse game between you and the attackers" - Haha, so true. When I saw the question, my mind immediately started thinking of ways to circumvent the restriction. One of the things I came up with is __loader__.load_module('o''s').system('ls')
    – marcelm
    Commented Jan 29 at 13:08
  • 3
    Also add a time limit or quota on the execution to avoid DOS attacks by infinite loops and the like
    – Bergi
    Commented Jan 29 at 20:45
  • 2
    Related question: stackoverflow.com/q/13066594/510937 to which I provided an exploit that uses a very limited character set (only lower case letters and the symbols +-*/() and in addition it does not contain the string eval): stackoverflow.com/a/13321536/510937
    – Bakuriu
    Commented Jan 30 at 6:51
  • 2
    The point is to have multiple layers of protection in case one of the measures fails. Sure, you could run arbitrary code if you assume it's perfectly sandboxed, but configuration issues and bugs that affect sandboxing features can and do happen. Because of this, I would definitely check the code. This also allows you to give more precise feedback to the user along the lines of "Sorry, this Python feature is prohibited, you can only use ..."
    – Ja1024
    Commented Jan 31 at 1:52
13

Try Lua

If you just need to run user-supplied scripts and aren't too concerned about what language the scripts are written in, you might find Lua a better fit than Python.

Lua is designed to allow you, as the developer of the program that's going to run the scripts, to be in control of what those scripts can do. Python doesn't really do this, and from what I've heard, the various attempts to make a more "restricted" Python do address a lot of scenarios but still have exploits that malicious actors can abuse. Lua is a popular choice for game modding exactly because of this.

(I don't actually know much Lua myself, I'm just aware that it's the usual tool chosen in this situation.)

2
  • 3
    Since it's a Rust app, you could use Rhai, a natively-Rust embedded language that has Rust-ish syntax with Python-ish semantics. You can export the precise functions you'd like to provide to the user code, and you can limit the code complexity or number of runtime operations.
    – Danya02
    Commented Jan 29 at 16:44
  • 4
    Be aware that Lua also needs some effort for secure sandboxing: most obviously disabling most features from io and debug libraries as well as disabling bytecode loading (which is loaded without validation by design). Its string processing is also vulnerable to algorithmic complexity attacks in native code (thus no easy way to interrupt it from within the process) and causing massive memory usage is well possible, so you'll probably want to run it as a separate sandboxed process. Commented Jan 30 at 4:21
6

Defending against arbitrary code execution while allowing code execution is not an easy problem. Most web browsers use a defence-in-depth approach to this, and you should too.

For example:

  1. Use either RestrictedPython or PyPy sandbox to limit what the scripts are allowed to do.
  2. Use process-level sandboxing to limit what the executing process is allowed to do.
  3. Create a separate, limited permissions user account for the execution.
  4. Implement audit logging to a secure location, so that if something happens, you can at least figure out where the hole was.

If you want, you can insert basic limitations like blacklisted keywords, script length limits and script runtime limits. But these should be considered more as usability features (better error messages to user) rather than security features.

2
  • 3
    The "Python Restricted Execution" link goes to a 21-year-old document, and the modules listed there were removed in Python 2.3, also 21 years ago.
    – jwodder
    Commented Jan 29 at 16:33
  • @jwodder Uh, you are correct. Looks like the best modern equivalent is the 3rd party RestrictedPython module.
    – jpa
    Commented Jan 29 at 17:33

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .