Repository navigation
HowTo: Embed and Sandbox User Defined Code
HowTo: Embed and Sandbox User-Defined Code
Allow your application or script to be extended / instrumented by safely embedding, sandboxing, and executing user-defined code
Create the Program Logic Container
Embedding Qore is performed with the Program class which serves as the logic container for Qore code; sandboxing restrictions and control over the API present in the Program object are specified with a binary or'ed combination of parse options.
For example, to allow code to be embedded with the basic Qore language API but with only the possibility to execute logic (I/O, threading, external access, etc prohibited), then you can create your Program object with the PO_LOCKDOWN parse restriction as in the following example:
Program p(PO_LOCKDOWN);Some other example sandboxing restriction/permission constants are listed in the following table:
| Sandbox Option | Description |
|---|---|
| PO_ALLOW_INJECTION | Allows code/dependency injections in the Program container |
| PO_NO_DATABASE | Prohibits access to DB functionality in the Program container |
| PO_NO_EXTERNAL_INFO | Prohibits access to information about the external computing environment (see %no-external-info for more info) |
| PO_NO_EXTERNAL_PROCESS | Prohibits access to external process functionality (ex: system(), exec(), backquote(), etc) |
| PO_NO_FILESYSTEM | Disallows access to the filesystem |
| PO_NO_MODULES | Disallows accessing or loading modules |
| PO_NO_NETWORK | Prohibits accessing the network |
| PO_NO_PROCESS_CONTROL | Prohibits access to functionality that affects the process as a whole (abort(), exit(), exec(), fork(), and many more) |
| PO_NO_TERMINAL_IO | Disallows input or output to the terminal |
| PO_NO_THREAD_CONTROL | Prohibits access to functionality related to starting or terminating threads, as well as thread-local data and managing thread resources |
Note:
- For the full list, see Parse Option Constants
- if PO_NO_CHILD_PO_RESTRICTIONS is set in a Program container, then embedded code in that Program container can escape the sandbox; this option is not set by default and must be set explicitly because it allows child Program objects to have fewer restrictions than the parent. By default (without this option), child Program objects are restricted to the Parse Options set in the parent.
Define the Program's API
To restrict the user and system API symbols available to the Program container, use the PO_NO_API, PO_NO_SYSTEM_API, or the PO_NO_USER_API parse options. These parse options control the API that's imported into the Program container; by default all system symbols are imported and all user symbols declared public are imported; using the preceding parse options can override this behavior.
Note: System symbols that are imported into a Program container may not be accessible due to other parse restrictions; for example, if PO_NO_FILESYSTEM is set, then the ReadOnlyFile class cannot be used, even though the symbol is imported in the Program container.
A custom API can be provided to the embedded code by calling Program::importClass(), Program::importFunction(), and Program::importGlobalVariable() (among others) as in the following example:
Program pgm(PO_NEW_STYLE|PO_STRICT_ARGS|PO_REQUIRE_TYPES|PO_NO_FILESYSTEM);
pgm.importClass("SecureFile");
pgm.importFunction("secure_hstat");Once the Program object has been created, code can then be imported into the object with the Program::parse() method.
Alternatively, to parse code from multiple sources into a Program object and maintain the source locations for that code, use the Program::parsePending() and Program::parseCommit() methods.
Note: All of the preceding methods have variants to accept a warning mask, which allows for a hash of warning information to be returned for potential warning logging and reporting.
For example:
# source_list is a list of hashes with "code" and "source_label" keys
Program sub get_program(list source_list) {
# create the Program
Program pgm(PO_NO_THREAD_CONTROL|PO_NO_PROCESS_CONTROL);
# a list of warning information
list wl = ();
# add warnings to the list if they exist
code parse = sub (string code, string source_label) {
*hash h = pgm.parsePending(code, source_label, WARN_DEFAULT|WARN_DUPLICATE_GLOBAL_VARS);
if (h)
wl += h;
};
# parse the code into the Program
map parse($1.code, $1.source_label), code_list;
# print warnings on stderr if "verbose" is set
if (wl && verbose)
map stderr.printf("WARNING: %s: %s: %s\n", get_ex_pos($1), $1.err, $1.desc), wl;
return pgm;
}Once the code has been parsed and loaded into the Program object, then you can run it with Program::run(), or you can call functions with Program::callFunction() or Program::callFunctionArgs() as in the following example:
%new-style
%strict-args
%require-types
%enable-all-warnings
list source_list = (
(
"code": "string sub get_message() { return \"my message\"; }",
"source_label": "my_code.q",
),
);
Program pgm = get_program(source_list);
string msg = pgm.callFunction("get_message");Note: Program objects can be created and destroyed at runtime. Be aware that any objects created from a Program object that is destroyed will become invalid when the Program is destroyed. Object factories can be implemented in Program objects that persist at least as long as the lifetime of the objects created from them, or alternatively from user modules. User modules always persist for the entire lifetime of the process; it's not possible to unload or reload a user module once loaded (except in the special case of code injections which is meant for dependency injections and requires the PO_ALLOW_INJECTION parse option).