Add security dialog to access files from scripts

This commit is contained in:
David Capello 2018-09-17 13:14:56 -03:00
parent aed23f4751
commit f6e3aa9fd8
12 changed files with 350 additions and 39 deletions

View File

@ -547,6 +547,7 @@
<icon part="check_normal" align="left middle" x="2" />
<icon part="check_selected" align="left middle" x="2" state="selected" />
<icon part="check_disabled" align="left middle" x="2" state="disabled" />
<icon part="check_selected" align="left middle" x="2" state="disabled selected" />
</style>
<style id="radio_button" border="2">
<background color="radio_hot_face" state="mouse focus" />

View File

@ -1105,6 +1105,19 @@ from = From:
to = To:
tolerance = Tolerance:
[script_access]
title = Security
script_label = The following script:
file_label = wants to access to this file:
command_label = wants to execute the following command:
dont_show_for_this_access = Don't show this specific alert again for this script
dont_show_for_this_script = Give full trust to this script
allow_execute_access = &Allow Execute Access
allow_write_access = &Allow Write Access
allow_read_access = &Allow Read Access
give_full_access = Give Script Full &Access
stop_script = &Stop Script
[select_accelerator]
title = Keyboard Shortcut
key = Key:

View File

@ -0,0 +1,22 @@
<!-- Aseprite -->
<!-- Copyright (C) 2018 by David Capello -->
<gui>
<window id="script_access" text="@.title">
<vbox>
<label text="@.script_label" />
<hbox border="4"><link id="script" /></hbox>
<label id="file_label" />
<hbox border="4"><link id="file" /></hbox>
<separator horizontal="true" />
<check id="dont_show" text="@.dont_show_for_this_access" />
<check id="full" text="@.dont_show_for_this_script" />
<separator horizontal="true" />
<grid columns="1">
<hbox homogeneous="true" cell_align="center bottom horizontal">
<button id="allow" closewindow="true" magnet="true" minwidth="60" />
<button id="stop" text="@.stop_script" closewindow="true" />
</hbox>
</grid>
</vbox>
</window>
</gui>

View File

@ -168,6 +168,7 @@ if(ENABLE_SCRIPTING)
script/pixel_color_object.cpp
script/point_class.cpp
script/rectangle_class.cpp
script/security.cpp
script/selection_class.cpp
script/site_class.cpp
script/size_class.cpp

View File

@ -88,6 +88,7 @@ private:
SaveFileBaseCommand::SaveFileBaseCommand(const char* id, CommandFlags flags)
: Command(id, flags)
{
m_useUI = true;
}
void SaveFileBaseCommand::onLoadParams(const Params& params)
@ -109,6 +110,9 @@ void SaveFileBaseCommand::onLoadParams(const Params& params)
m_selFrames.clear();
m_adjustFramesByFrameTag = false;
}
std::string useUI = params.get("useUI");
m_useUI = (useUI.empty() || (useUI == "true"));
}
// Returns true if there is a current sprite to save.
@ -139,7 +143,8 @@ std::string SaveFileBaseCommand::saveAsDialog(
#ifdef ENABLE_UI
again:;
base::paths newfilename;
if (!app::show_file_selector(
if (!m_useUI ||
!app::show_file_selector(
dlgTitle, filename, exts,
FileSelectorType::Save,
newfilename))
@ -316,7 +321,7 @@ void SaveFileCopyAsCommand::onExecute(Context* context)
bool isForTwitter = false;
#if ENABLE_UI
if (context->isUIAvailable()) {
if (m_useUI && context->isUIAvailable()) {
ExportFileWindow win(doc);
bool askOverwrite = true;

View File

@ -44,6 +44,7 @@ namespace app {
std::string m_slice;
doc::SelectedFrames m_selFrames;
bool m_adjustFramesByFrameTag;
bool m_useUI;
};
} // namespace app

View File

@ -17,11 +17,13 @@
#include "app/pref/preferences.h"
#include "app/script/engine.h"
#include "app/script/luacpp.h"
#include "app/script/security.h"
#include "app/site.h"
#include "app/tx.h"
#include "app/ui/doc_view.h"
#include "app/ui/editor/editor.h"
#include "app/ui_context.h"
#include "base/fs.h"
#include "doc/layer.h"
#include "ui/alert.h"
@ -34,7 +36,10 @@ namespace {
int App_open(lua_State* L)
{
const char* filename = luaL_checkstring(L, 1);
std::string absFn = base::get_absolute_path(luaL_checkstring(L, 1));
if (!ask_access(L, absFn.c_str(), FileAccessMode::Read, true))
return luaL_error(L, "script doesn't have access to open file %s",
absFn.c_str());
app::Context* ctx = App::instance()->context();
Doc* oldDoc = ctx->activeDocument();
@ -42,7 +47,7 @@ int App_open(lua_State* L)
Command* openCommand =
Commands::instance()->byId(CommandId::OpenFile());
Params params;
params.set("filename", filename);
params.set("filename", absFn.c_str());
ctx->executeCommand(openCommand, params);
Doc* newDoc = ctx->activeDocument();

View File

@ -13,6 +13,7 @@
#include "app/app.h"
#include "app/console.h"
#include "app/script/luacpp.h"
#include "app/script/security.h"
#include "base/chrono.h"
#include "base/file_handle.h"
#include "base/fs.h"
@ -140,7 +141,7 @@ Engine::Engine()
lua_register(L, "print", print);
lua_getglobal(L, "os");
for (const char* name : { "execute", "remove", "rename", "exit", "tmpname" }) {
for (const char* name : { "remove", "rename", "exit", "tmpname" }) {
lua_pushcfunction(L, unsupported);
lua_setfield(L, -2, name);
}
@ -148,6 +149,20 @@ Engine::Engine()
lua_setfield(L, -2, "clock");
lua_pop(L, 1);
// Wrap io.open()
lua_getglobal(L, "io");
lua_getfield(L, -1, "open");
lua_pushcclosure(L, secure_io_open, 1);
lua_setfield(L, -2, "open");
lua_pop(L, 1);
// Wrap os.execute()
lua_getglobal(L, "os");
lua_getfield(L, -1, "execute");
lua_pushcclosure(L, secure_os_execute, 1);
lua_setfield(L, -2, "execute");
lua_pop(L, 1);
// Generic code used by metatables
run_mt_index_code(L);
@ -270,19 +285,8 @@ bool Engine::evalFile(const std::string& filename)
std::ifstream s(FSTREAM_PATH(filename));
buf << s.rdbuf();
}
std::string fn = filename;
if (fn.size() > 2 &&
#ifdef _WIN32
fn[1] != ':'
#else
fn[0] != '/'
#endif
) {
fn = base::join_path(base::get_current_path(), fn);
}
fn = base::get_canonical_path(fn);
return evalCode(buf.str(), "@" + fn);
std::string absFilename = base::get_absolute_path(filename);
return evalCode(buf.str(), "@" + absFilename);
}
void Engine::onConsolePrint(const char* text)

View File

@ -37,6 +37,13 @@ namespace app {
class Site;
namespace script {
enum class FileAccessMode {
Execute = 1,
Write = 2,
Read = 4,
Full = 7
};
class EngineDelegate {
public:
virtual ~EngineDelegate() { }

205
src/app/script/security.cpp Normal file
View File

@ -0,0 +1,205 @@
// Aseprite
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/script/security.h"
#include "app/app.h"
#include "app/context.h"
#include "app/i18n/strings.h"
#include "app/ini_file.h"
#include "app/launcher.h"
#include "app/script/engine.h"
#include "app/script/luacpp.h"
#include "base/convert_to.h"
#include "base/fs.h"
#include "base/sha1.h"
#include "fmt/format.h"
#include "script_access.xml.h"
#include <algorithm>
#include <cstring>
#include <unordered_map>
namespace app {
namespace script {
namespace {
// Map from .lua file name -> sha1
std::unordered_map<std::string, std::string> g_keys;
std::string get_key(const std::string& source)
{
auto it = g_keys.find(source);
if (it != g_keys.end())
return it->second;
else
return g_keys[source] = base::convert_to<std::string>(
base::Sha1::calculateFromString(source));
}
std::string get_script_filename(lua_State* L)
{
// Get script name
lua_getglobal(L, "debug");
lua_getfield(L, -1, "getinfo");
lua_remove(L, -2);
lua_pushinteger(L, 2);
lua_pushstring(L, "S");
lua_call(L, 2, 1);
lua_getfield(L, -1, "source");
const char* source = lua_tostring(L, -1);
std::string script;
if (source && *source)
script = source+1;
lua_pop(L, 2);
return script;
}
} // anonymous namespace
int secure_io_open(lua_State* L)
{
int n = lua_gettop(L);
std::string absFilename = base::get_absolute_path(lua_tostring(L, 1));
FileAccessMode mode = FileAccessMode::Read; // Read is the default access
if (lua_tostring(L, 2) &&
std::strchr(lua_tostring(L, 2), 'w') != nullptr) {
mode = FileAccessMode::Write;
}
if (!ask_access(L, absFilename.c_str(), mode, true)) {
return luaL_error(L, "the script doesn't have access to file '%s'",
absFilename.c_str());
}
lua_pushvalue(L, lua_upvalueindex(1));
lua_pushstring(L, absFilename.c_str());
for (int i=2; i<=n; ++i)
lua_pushvalue(L, i);
lua_call(L, n, 1);
return 1;
}
int secure_os_execute(lua_State* L)
{
int n = lua_gettop(L);
if (n == 0)
return 0;
const char* cmd = lua_tostring(L, 1);
if (!ask_access(L, cmd, FileAccessMode::Execute, false)) {
// Stop script
return luaL_error(L, "the script doesn't have access to execute the command: '%s'",
cmd);
}
lua_pushvalue(L, lua_upvalueindex(1));
for (int i=1; i<=n; ++i)
lua_pushvalue(L, i);
lua_call(L, n, 1);
return 1;
}
bool ask_access(lua_State* L,
const char* filename,
const FileAccessMode mode,
const bool canOpenFile)
{
#ifdef ENABLE_UI
// Ask for permission to open the file
if (App::instance()->context()->isUIAvailable()) {
std::string script = get_script_filename(L);
if (script.empty()) // No script
return luaL_error(L, "no debug information (script filename) to secure io.open() call");
const char* section = "script_access";
std::string key = get_key(script);
int access = get_config_int(section, key.c_str(), 0);
// Has the correct access
if ((access & int(mode)) == int(mode))
return true;
std::string allowButtonText =
mode == FileAccessMode::Execute ?
Strings::script_access_allow_execute_access():
mode == FileAccessMode::Write ?
Strings::script_access_allow_write_access():
Strings::script_access_allow_read_access();
app::gen::ScriptAccess dlg;
dlg.script()->setText(script);
dlg.fileLabel()->setText(
canOpenFile ?
Strings::script_access_file_label():
Strings::script_access_command_label());
dlg.file()->setText(filename);
dlg.allow()->setText(allowButtonText);
dlg.allow()->processMnemonicFromText();
dlg.script()->Click.connect(
[&dlg]{
app::launcher::open_folder(dlg.script()->text());
});
dlg.full()->Click.connect(
[&dlg, &allowButtonText](ui::Event&){
if (dlg.full()->isSelected()) {
dlg.dontShow()->setSelected(true);
dlg.dontShow()->setEnabled(false);
dlg.allow()->setText(Strings::script_access_give_full_access());
dlg.allow()->processMnemonicFromText();
dlg.layout();
}
else {
dlg.dontShow()->setEnabled(true);
dlg.allow()->setText(allowButtonText);
dlg.allow()->processMnemonicFromText();
dlg.layout();
}
});
if (canOpenFile) {
dlg.file()->Click.connect(
[&dlg]{
std::string fn = dlg.file()->text();
if (base::is_file(fn))
app::launcher::open_folder(fn);
else
app::launcher::open_folder(base::get_file_path(fn));
});
}
dlg.openWindowInForeground();
const bool allow = (dlg.closer() == dlg.allow());
// Save selected option
if (allow && dlg.dontShow()->isSelected()) {
if (dlg.full()->isSelected())
set_config_int(section, key.c_str(), access | int(FileAccessMode::Full));
else
set_config_int(section, key.c_str(), access | int(mode));
flush_config_file();
}
if (!allow)
return false;
}
#endif
return true;
}
} // namespace script
} // namespace app

31
src/app/script/security.h Normal file
View File

@ -0,0 +1,31 @@
// Aseprite
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_SCRIPT_SECURITY_H_INCLUDED
#define APP_SCRIPT_SECURITY_H_INCLUDED
#pragma once
#ifndef ENABLE_SCRIPTING
#error ENABLE_SCRIPTING must be defined
#endif
#include "app/script/engine.h"
namespace app {
namespace script {
int secure_io_open(lua_State* L);
int secure_os_execute(lua_State* L);
bool ask_access(lua_State* L,
const char* filename,
const FileAccessMode mode,
const bool canOpenFile);
} // namespace script
} // namespace app
#endif

View File

@ -24,10 +24,12 @@
#include "app/file/palette_file.h"
#include "app/script/engine.h"
#include "app/script/luacpp.h"
#include "app/script/security.h"
#include "app/site.h"
#include "app/transaction.h"
#include "app/tx.h"
#include "app/ui/doc_view.h"
#include "base/fs.h"
#include "doc/frame_tag.h"
#include "doc/layer.h"
#include "doc/mask.h"
@ -114,8 +116,9 @@ int Sprite_crop(lua_State* L)
return 0;
}
int Sprite_saveAs(lua_State* L)
int Sprite_saveAs_base(lua_State* L, std::string& absFn)
{
bool result = false;
auto sprite = get_ptr<Sprite>(L, 1);
const char* fn = luaL_checkstring(L, 2);
if (fn && sprite) {
@ -123,35 +126,43 @@ int Sprite_saveAs(lua_State* L)
app::Context* appCtx = App::instance()->context();
appCtx->setActiveDocument(doc);
absFn = base::get_absolute_path(fn);
if (!ask_access(L, absFn.c_str(), FileAccessMode::Write, true))
return luaL_error(L, "script doesn't have access to write file %s",
absFn.c_str());
Command* saveCommand =
Commands::instance()->byId(CommandId::SaveFileCopyAs());
Params params;
params.set("filename", fn);
appCtx->executeCommand(saveCommand);
params.set("filename", absFn.c_str());
params.set("useUI", "false");
appCtx->executeCommand(saveCommand, params);
doc->setFilename(fn);
result = true;
}
return 0;
lua_pushboolean(L, result);
return 1;
}
int Sprite_saveAs(lua_State* L)
{
std::string fn;
int res = Sprite_saveAs_base(L, fn);
if (!fn.empty()) {
auto sprite = get_ptr<Sprite>(L, 1);
if (sprite) {
Doc* doc = static_cast<Doc*>(sprite->document());
doc->setFilename(fn);
}
}
return res;
}
int Sprite_saveCopyAs(lua_State* L)
{
auto sprite = get_ptr<Sprite>(L, 1);
const char* fn = luaL_checkstring(L, 2);
if (fn && sprite) {
Doc* doc = static_cast<Doc*>(sprite->document());
app::Context* appCtx = App::instance()->context();
appCtx->setActiveDocument(doc);
Command* saveCommand =
Commands::instance()->byId(CommandId::SaveFileCopyAs());
Params params;
params.set("filename", fn);
appCtx->executeCommand(saveCommand, params);
}
return 0;
std::string fn;
return Sprite_saveAs_base(L, fn);
}
int Sprite_loadPalette(lua_State* L)
@ -159,8 +170,13 @@ int Sprite_loadPalette(lua_State* L)
auto sprite = get_ptr<Sprite>(L, 1);
const char* fn = luaL_checkstring(L, 2);
if (fn && sprite) {
std::string absFn = base::get_absolute_path(fn);
if (!ask_access(L, absFn.c_str(), FileAccessMode::Read, true))
return luaL_error(L, "script doesn't have access to open file %s",
absFn.c_str());
Doc* doc = static_cast<Doc*>(sprite->document());
std::unique_ptr<doc::Palette> palette(load_palette(fn));
std::unique_ptr<doc::Palette> palette(load_palette(absFn.c_str()));
if (palette) {
Tx tx;
// TODO Merge this with the code in LoadPaletteCommand