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
32namespace app {
33namespace script {
34
35#ifdef ENABLE_UI
36namespace {
37
38// Map from .lua file name -> sha1
39std::unordered_map<std::string, std::string> g_keys;
40
41std::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
51std::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
72int 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
97int 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
117bool 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