1 | // Aseprite |
2 | // Copyright (C) 2019-2021 Igara Studio S.A. |
3 | // Copyright (C) 2018 David Capello |
4 | // |
5 | // This program is distributed under the terms of |
6 | // the End-User License Agreement for Aseprite. |
7 | |
8 | #ifdef HAVE_CONFIG_H |
9 | #include "config.h" |
10 | #endif |
11 | |
12 | #include "app/script/security.h" |
13 | |
14 | #include "app/app.h" |
15 | #include "app/context.h" |
16 | #include "app/i18n/strings.h" |
17 | #include "app/ini_file.h" |
18 | #include "app/launcher.h" |
19 | #include "app/script/engine.h" |
20 | #include "app/script/luacpp.h" |
21 | #include "base/convert_to.h" |
22 | #include "base/fs.h" |
23 | #include "base/sha1.h" |
24 | #include "fmt/format.h" |
25 | |
26 | #include "script_access.xml.h" |
27 | |
28 | #include <algorithm> |
29 | #include <cstring> |
30 | #include <unordered_map> |
31 | |
32 | namespace app { |
33 | namespace script { |
34 | |
35 | #ifdef ENABLE_UI |
36 | namespace { |
37 | |
38 | // Map from .lua file name -> sha1 |
39 | std::unordered_map<std::string, std::string> g_keys; |
40 | |
41 | std::string get_key(const std::string& source) |
42 | { |
43 | auto it = g_keys.find(source); |
44 | if (it != g_keys.end()) |
45 | return it->second; |
46 | else |
47 | return g_keys[source] = base::convert_to<std::string>( |
48 | base::Sha1::calculateFromString(source)); |
49 | } |
50 | |
51 | std::string get_script_filename(lua_State* L) |
52 | { |
53 | // Get script name |
54 | lua_getglobal(L, "debug" ); |
55 | lua_getfield(L, -1, "getinfo" ); |
56 | lua_remove(L, -2); |
57 | lua_pushinteger(L, 2); |
58 | lua_pushstring(L, "S" ); |
59 | lua_call(L, 2, 1); |
60 | lua_getfield(L, -1, "source" ); |
61 | const char* source = lua_tostring(L, -1); |
62 | std::string script; |
63 | if (source && *source) |
64 | script = source+1; |
65 | lua_pop(L, 2); |
66 | return script; |
67 | } |
68 | |
69 | } // anonymous namespace |
70 | #endif // ENABLE_UI |
71 | |
72 | int secure_io_open(lua_State* L) |
73 | { |
74 | int n = lua_gettop(L); |
75 | |
76 | std::string absFilename = base::get_absolute_path(lua_tostring(L, 1)); |
77 | |
78 | FileAccessMode mode = FileAccessMode::Read; // Read is the default access |
79 | if (lua_tostring(L, 2) && |
80 | std::strchr(lua_tostring(L, 2), 'w') != nullptr) { |
81 | mode = FileAccessMode::Write; |
82 | } |
83 | |
84 | if (!ask_access(L, absFilename.c_str(), mode, ResourceType::File)) { |
85 | return luaL_error(L, "the script doesn't have access to file '%s'" , |
86 | absFilename.c_str()); |
87 | } |
88 | |
89 | lua_pushvalue(L, lua_upvalueindex(1)); |
90 | lua_pushstring(L, absFilename.c_str()); |
91 | for (int i=2; i<=n; ++i) |
92 | lua_pushvalue(L, i); |
93 | lua_call(L, n, 1); |
94 | return 1; |
95 | } |
96 | |
97 | int secure_os_execute(lua_State* L) |
98 | { |
99 | int n = lua_gettop(L); |
100 | if (n == 0) |
101 | return 0; |
102 | |
103 | const char* cmd = lua_tostring(L, 1); |
104 | if (!ask_access(L, cmd, FileAccessMode::Execute, ResourceType::Command)) { |
105 | // Stop script |
106 | return luaL_error(L, "the script doesn't have access to execute the command: '%s'" , |
107 | cmd); |
108 | } |
109 | |
110 | lua_pushvalue(L, lua_upvalueindex(1)); |
111 | for (int i=1; i<=n; ++i) |
112 | lua_pushvalue(L, i); |
113 | lua_call(L, n, 1); |
114 | return 1; |
115 | } |
116 | |
117 | bool ask_access(lua_State* L, |
118 | const char* filename, |
119 | const FileAccessMode mode, |
120 | const ResourceType resourceType) |
121 | { |
122 | #ifdef ENABLE_UI |
123 | // Ask for permission to open the file |
124 | if (App::instance()->context()->isUIAvailable()) { |
125 | std::string script = get_script_filename(L); |
126 | if (script.empty()) // No script |
127 | return luaL_error(L, "no debug information (script filename) to secure io.open() call" ); |
128 | |
129 | const char* section = "script_access" ; |
130 | std::string key = get_key(script); |
131 | |
132 | int access = get_config_int(section, key.c_str(), 0); |
133 | |
134 | // Has the correct access |
135 | if ((access & int(mode)) == int(mode)) |
136 | return true; |
137 | |
138 | std::string allowButtonText = |
139 | mode == FileAccessMode::OpenSocket ? |
140 | Strings::script_access_allow_open_conn_access(): |
141 | mode == FileAccessMode::Execute ? |
142 | Strings::script_access_allow_execute_access(): |
143 | mode == FileAccessMode::Write ? |
144 | Strings::script_access_allow_write_access(): |
145 | Strings::script_access_allow_read_access(); |
146 | |
147 | app::gen::ScriptAccess dlg; |
148 | dlg.script()->setText(script); |
149 | |
150 | { |
151 | std::string label; |
152 | switch (resourceType) { |
153 | case ResourceType::File: label = Strings::script_access_file_label(); break; |
154 | case ResourceType::Command: label = Strings::script_access_command_label(); break; |
155 | case ResourceType::WebSocket: label = Strings::script_access_websocket_label(); break; |
156 | } |
157 | dlg.fileLabel()->setText(label); |
158 | } |
159 | |
160 | dlg.file()->setText(filename); |
161 | dlg.allow()->setText(allowButtonText); |
162 | dlg.allow()->processMnemonicFromText(); |
163 | |
164 | dlg.script()->Click.connect( |
165 | [&dlg]{ |
166 | app::launcher::open_folder(dlg.script()->text()); |
167 | }); |
168 | |
169 | dlg.full()->Click.connect( |
170 | [&dlg, &allowButtonText](ui::Event&){ |
171 | if (dlg.full()->isSelected()) { |
172 | dlg.dontShow()->setSelected(true); |
173 | dlg.dontShow()->setEnabled(false); |
174 | dlg.allow()->setText(Strings::script_access_give_full_access()); |
175 | dlg.allow()->processMnemonicFromText(); |
176 | dlg.layout(); |
177 | } |
178 | else { |
179 | dlg.dontShow()->setEnabled(true); |
180 | dlg.allow()->setText(allowButtonText); |
181 | dlg.allow()->processMnemonicFromText(); |
182 | dlg.layout(); |
183 | } |
184 | }); |
185 | |
186 | if (resourceType == ResourceType::File) { |
187 | dlg.file()->Click.connect( |
188 | [&dlg]{ |
189 | std::string fn = dlg.file()->text(); |
190 | if (base::is_file(fn)) |
191 | app::launcher::open_folder(fn); |
192 | else |
193 | app::launcher::open_folder(base::get_file_path(fn)); |
194 | }); |
195 | } |
196 | |
197 | dlg.openWindowInForeground(); |
198 | const bool allow = (dlg.closer() == dlg.allow()); |
199 | |
200 | // Save selected option |
201 | if (allow && dlg.dontShow()->isSelected()) { |
202 | if (dlg.full()->isSelected()) |
203 | set_config_int(section, key.c_str(), access | int(FileAccessMode::Full)); |
204 | else |
205 | set_config_int(section, key.c_str(), access | int(mode)); |
206 | flush_config_file(); |
207 | } |
208 | |
209 | if (!allow) |
210 | return false; |
211 | } |
212 | #endif |
213 | return true; |
214 | } |
215 | |
216 | } // namespace script |
217 | } // namespace app |
218 | |