PLUGIN sandbox lang: "C++2a" title: "Sandbox" author: "Julien TALLON" changelog: "" maintainer: "Julien TALLON Julien BRUGUIER " date: "2023-02-02" version: "1.0.0" synopsis: %{ A plugin to ensure accessed files are always contained within a folder. %} description: %{ This plugin implements file opening restrictions so that no files can be accessed outside the folder specified as sandbox. Any file or folder name can be provided absolute or relative interchangeably. It aims at being homogenous with com.file management from com plugin. %} example: "basic" %{ .nf :memory sandbox.folder/fld, com.device/file, STR/s :sandbox.new "testsandbox" -> &fld :com.open sandbox.file @&fld < "testsandbox/test.txt" -> &file :com.read @&file com.all -> &s :com.message @&file .fi %} seealso: %{ .BR svm_plugin_com (7) for com plugin documentation. %} test: initialisation %{ echo "ERROR This content shall not be accessible by test.svm" > badaccess.txt mkdir sandbox echo "SUCCESS This is legitimate content that is accessed/accessible !" > sandbox/test.txt mkdir sandbox/a echo "SUCCESS This is also legitimate content to extract for the test !" > sandbox/a/test.txt %} test: finalisation %{ rm -rf badaccess.txt sandbox %} test: "fileinsandboxaccessible" %{ PLUGIN "svmcom.so" PLUGIN "svmstr.so" PLUGIN "svmrun.so" PROCESS "main" CODE "main" INLINE :run.interruption "interruption" :memory sandbox.folder/fld, com.device/file, STR/s, INT*2/pos, BLN/testresult # Unique sandbox location for all tests :sandbox.new "sandbox" -> &fld # Most basic test case file directly in sandbox folder :com.open sandbox.file @&fld < "sandbox/test.txt" -> &file :com.read @&file com.all -> &s [0,0] -> pos :str.find @&s CONST str.pattern "SUCCESS" @(pos/0) @(pos/1) -> &testresult :shutdown 0 :when @&testresult TRUE :label fail :com.message "Result mismatch" :shutdown 1 :label interruption :run.coredump STDOUT :com.message "An unexpected interruption occurred" :shutdown 1 END END %} test: "filedeepinsandboxaccessible" %{ PLUGIN "svmcom.so" PLUGIN "svmstr.so" PLUGIN "svmrun.so" PROCESS "main" CODE "main" INLINE :run.interruption "interruption" :memory sandbox.folder/fld, com.device/file, STR/s, INT*2/pos, BLN/testresult # Unique sandbox location for all tests :sandbox.new "sandbox" -> &fld # More elaborate case with file in sandbox but through subfolder :com.open sandbox.file @&fld < "sandbox/a/test.txt" -> &file :com.read @&file com.all -> &s [0,0] -> pos :str.find @&s CONST str.pattern "SUCCESS" @(pos/0) @(pos/1) -> &testresult :shutdown 0 :when @&testresult TRUE :label fail :com.message "Result mismatch" :shutdown 1 :label interruption :run.coredump STDOUT :com.message "An unexpected interruption occurred" :shutdown 1 END END %} test: "filenotinsandboxunaccessible" %{ PLUGIN "svmcom.so" PLUGIN "svmstr.so" PLUGIN "svmrun.so" PROCESS "main" CODE "main" INLINE :run.interruption "interruption" :memory sandbox.folder/fld, com.device/file # Unique sandbox location for all tests :sandbox.new "sandbox" -> &fld # Negative case : file out of sandbox :interruption !sandbox.out success :com.open sandbox.file @&fld < "badaccess.txt" -> &file :label fail :com.message "Result mismatch" :shutdown 1 :label interruption :com.message "An unexpected interruption occurred" :shutdown 1 :label success END END %} patch: "configure.ac" %{ --- configure.ac.orig 2023-03-19 00:26:31.021989549 +0100 +++ configure.ac 2023-03-19 00:27:05.481073672 +0100 @@ -39,6 +39,7 @@ CXXFLAGS="$CXXFLAGS $AM_CXXFLAGS" AC_HEADER_ASSERT +AC_CHECK_HEADERS([filesystem],,[AC_MSG_ERROR([Header file required to compile not available !])]) AC_CHECK_HEADERS([/usr/local/include/svm/svm.h /usr/local/include/svm/svm_compatibility.h],,[AC_MSG_ERROR([Header file required to compile not available !])]) CXXFLAGS=$CXXFLAGS_OLD %} includes: %{ #include #include #include #include %} initialisation: %{ %} finalisation: %{ %} code: %{ %} USE STRUCT com.file help: %{} TYPE com.device help: %{} FUNCTION com.device_file_close $com.file -> BLN help: %{} FUNCTION com.device_file_command $com.file . * -> VALUE ? help: %{} FUNCTION com.device_file_open [ < > <> >> ] STR [ 'EXEC' 'PRIV' ] * -> $com.file help: %{} FUNCTION com.device_file_print $com.file -> STR help: %{} FUNCTION com.device_file_read $com.file -> STR ? help: %{} FUNCTION com.device_file_write $com.file STR help: %{} DEFINE TYPE sandbox.folder %{ type_folder(const std::string& iStr) : _path(iStr) {} type_folder(const type_folder& iRhs) = default; type_folder(type_folder&& iRhs) = default; operator std::string() const { std::stringstream out; out << "Path : " << _path.string() << "\nRealpath : " << _canonicalpath.string(); return out.str(); } std::filesystem::path _path; std::filesystem::path _canonicalpath; %} delete default: %{} copy default: %{} constant default: %{ std::error_code err, direrr; object->_canonicalpath = std::filesystem::canonical(object->_path, err); if(err != std::error_code()) { std::stringstream out; out << "Requested folder : " << object->_path.string() << " not found on filesystem."; ERROR_INTERNAL(FAILURE, out.str().c_str()); } bool isdir = std::filesystem::is_directory(object->_canonicalpath, direrr); if(!isdir || (direrr != std::error_code())) { std::stringstream out; out << "Requested folder : " << object->_path.string() << " is not a directory."; ERROR_INTERNAL(FAILURE, out.str().c_str()); } %} print default: %{} help: %{ Contains a folder path validated as such at construction time. It represents the desired sandbox location. The path provided must exist on the filesystem and match a directory. This type supports copy, constant construction and string rendering. %} INSTRUCTION sandbox.new STR -> sandbox.folder %{ return NEW_PLUGIN(sandbox, folder, ::type_folder_constant(svm, ARGV_VALUE(0,string))); %} help: %{ Creates a verified sandbox.folder object from a string. %} STRUCT sandbox.file %{ SVM_Structure _comfile; %} # No copy necessary, a com.device is not copyable and we compose with one delete default: %{ ::svm_variable_scope_set_local(svm, object->_comfile); %} help: %{ Represents a sandboxed file. %} FUNCTION sandbox.device_file_open sandbox.folder [ < > <> >> ] STR [ 'EXEC' 'PRIV' ] * -> $sandbox.file %{ auto folder = ARGV_PLUGIN(0, sandbox, folder); std::string mode = ARGV_MARKER(1); SVM_String filenamesvmstr = ARGV_VALUE(2, string); auto filenamestr = RAW_STRING(filenamesvmstr); auto filenamepath = std::filesystem::path(filenamestr).remove_filename(); if(filenamepath.empty()) { filenamepath = std::filesystem::current_path(); } std::error_code err; auto canonicalfilename = std::filesystem::canonical(filenamepath, err); if(err != std::error_code()) { std::stringstream outstream; outstream << "Requested file : " << filenamestr << " not found on filesystem."; ERROR_INTERNAL(FAILURE, outstream.str().c_str()); } std::stringstream debuggerevent; debuggerevent << std::string(*folder) << " | " << filenamestr << " " << canonicalfilename.string(); ::svm_debug_notify__raw(svm, CURRENT(kernel), debuggerevent.str().c_str()); if(!canonicalfilename.string().starts_with(folder->_canonicalpath.string())) { std::stringstream outstream; outstream << "Requested file : "<< filenamestr << " is not included in the sandbox folder : " << folder->_path.string(); ERROR_EXTERNAL(sandbox, out, outstream.str().c_str()); } auto comfile = ::svm_function_call(svm, CONST_PEP(com, device_file_open), argc-1, argv+1); ::svm_variable_scope_set_global(svm, comfile); return NEW_STRUCT(sandbox, file, new struct_file{comfile}); %} help: %{ This function implements com.open instruction for sandbox.file struct. Notice the addition of the sandbox.folder parameter compared to the classic com.file struct usage of com.open instruction. It opens a sandboxed file device and reserves the corresponding system resources. Otherwise it will raise an interruption: - FAILURE when the path of the file to open does not exist on the filesystem. - com.interrupted for any issue in managing the underlying com.file of the sandboxed file. - sandbox.out when the file to open is not contained in the sandbox. %} FUNCTION sandbox.device_file_read $sandbox.file -> STR ? %{ auto* sandboxfile = ARGV_STRUCT(0, sandbox, file); argv[0] = ::svm_parameter_structure_new(svm, sandboxfile->_comfile); return ::svm_function_call(svm, CONST_PEP(com, device_file_read), argc, argv); %} help: %{ This function implements com.read instruction for a sandbox.file file. More details on .BR svm_plugin_com (7) %} FUNCTION sandbox.device_file_write $sandbox.file STR %{ auto* sandboxfile = ARGV_STRUCT(0, sandbox, file); argv[0] = ::svm_parameter_structure_new(svm, sandboxfile->_comfile); ::svm_function_call(svm, CONST_PEP(com, device_file_write), argc, argv); %} help: %{ This function implements com.write instruction for a sandbox.file struct. More details on .BR svm_plugin_com (7) %} FUNCTION sandbox.device_file_close $sandbox.file -> BLN %{ auto* sandboxfile = ARGV_STRUCT(0, sandbox,file); SVM_Parameter* p = ::svm_parameter_array_new(svm, 1); p[0] = ::svm_parameter_structure_new(svm, sandboxfile->_comfile); return ::svm_function_call(svm, CONST_PEP(com, device_file_close), 1, p); %} help: %{ This function implements closure of a sandbox.file struct. %} FUNCTION sandbox.device_file_print $sandbox.file -> STR %{ auto* sandboxfile = ARGV_STRUCT(0, sandbox, file); argv[0] = ::svm_parameter_structure_new(svm, sandboxfile->_comfile); SVM_String output = ::svm_value_string_get(svm, ::svm_function_call(svm, CONST_PEP(com, device_file_print), argc, argv)); auto realoutput = RAW_STRING(output); using std::string_literals::operator""s; realoutput = "Sandboxed"s + realoutput; return NEW_VALUE(string,::svm_string_new(svm, realoutput.c_str(), realoutput.size())); %} help: %{ This function implements string casting for a sanbox.file type. %} FUNCTION sandbox.device_file_command $sandbox.file . * -> VALUE ? %{ auto* sandboxfile = ARGV_STRUCT(0, sandbox, file); argv[0] = ::svm_parameter_structure_new(svm, sandboxfile->_comfile); return ::svm_function_call(svm, CONST_PEP(com, device_file_command), argc, argv); %} help: %{ This function implements the same features as com.command instruction for com.file struct but for a sandbox.file struct. %} INTERRUPTION sandbox.out help: %{ This interruption is raised by com.open instruction when the required file to open is not part of the sandbox folder. %}