macOS: Always require confirmation when executing a script via `open` (Finder, etc.) (#8442)
Right now, passing a file path to Ghostty will always execute it unconditionally. This has various risks associated with it. I think we can mitigate a lot of risks in the future by inspecting what is being executed, but to be safe now we should always ask for confirmation.pull/8448/head
commit
6cfd89e248
|
|
@ -419,6 +419,7 @@ typedef struct {
|
||||||
ghostty_env_var_s* env_vars;
|
ghostty_env_var_s* env_vars;
|
||||||
size_t env_var_count;
|
size_t env_var_count;
|
||||||
const char* initial_input;
|
const char* initial_input;
|
||||||
|
bool wait_after_command;
|
||||||
} ghostty_surface_config_s;
|
} ghostty_surface_config_s;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
|
||||||
|
|
@ -394,35 +394,69 @@ class AppDelegate: NSObject,
|
||||||
// Ghostty will validate as well but we can avoid creating an entirely new
|
// Ghostty will validate as well but we can avoid creating an entirely new
|
||||||
// surface by doing our own validation here. We can also show a useful error
|
// surface by doing our own validation here. We can also show a useful error
|
||||||
// this way.
|
// this way.
|
||||||
|
|
||||||
var isDirectory = ObjCBool(true)
|
var isDirectory = ObjCBool(true)
|
||||||
guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false }
|
guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false }
|
||||||
|
|
||||||
|
// Set to true if confirmation is required before starting up the
|
||||||
|
// new terminal.
|
||||||
|
var requiresConfirm: Bool = false
|
||||||
|
|
||||||
// Initialize the surface config which will be used to create the tab or window for the opened file.
|
// Initialize the surface config which will be used to create the tab or window for the opened file.
|
||||||
var config = Ghostty.SurfaceConfiguration()
|
var config = Ghostty.SurfaceConfiguration()
|
||||||
|
|
||||||
if (isDirectory.boolValue) {
|
if (isDirectory.boolValue) {
|
||||||
// When opening a directory, check the configuration to decide
|
// When opening a directory, check the configuration to decide
|
||||||
// whether to open in a new tab or new window.
|
// whether to open in a new tab or new window.
|
||||||
config.workingDirectory = filename
|
config.workingDirectory = filename
|
||||||
} else {
|
} else {
|
||||||
|
// Unconditionally require confirmation in the file execution case.
|
||||||
|
// In the future I have ideas about making this more fine-grained if
|
||||||
|
// we can not inherit of unsandboxed state. For now, we need to confirm
|
||||||
|
// because there is a sandbox escape possible if a sandboxed application
|
||||||
|
// somehow is tricked into `open`-ing a non-sandboxed application.
|
||||||
|
requiresConfirm = true
|
||||||
|
|
||||||
// When opening a file, we want to execute the file. To do this, we
|
// When opening a file, we want to execute the file. To do this, we
|
||||||
// don't override the command directly, because it won't load the
|
// don't override the command directly, because it won't load the
|
||||||
// profile/rc files for the shell, which is super important on macOS
|
// profile/rc files for the shell, which is super important on macOS
|
||||||
// due to things like Homebrew. Instead, we set the command to
|
// due to things like Homebrew. Instead, we set the command to
|
||||||
// `<filename>; exit` which is what Terminal and iTerm2 do.
|
// `<filename>; exit` which is what Terminal and iTerm2 do.
|
||||||
config.initialInput = "\(filename); exit\n"
|
config.initialInput = "\(filename); exit\n"
|
||||||
|
|
||||||
|
// For commands executed directly, we want to ensure we wait after exit
|
||||||
|
// because in most cases scripts don't block on exit and we don't want
|
||||||
|
// the window to just flash closed once complete.
|
||||||
|
config.waitAfterCommand = true
|
||||||
|
|
||||||
// Set the parent directory to our working directory so that relative
|
// Set the parent directory to our working directory so that relative
|
||||||
// paths in scripts work.
|
// paths in scripts work.
|
||||||
config.workingDirectory = (filename as NSString).deletingLastPathComponent
|
config.workingDirectory = (filename as NSString).deletingLastPathComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if requiresConfirm {
|
||||||
|
// Confirmation required. We use an app-wide NSAlert for now. In the future we
|
||||||
|
// may want to show this as a sheet on the focused window (especially if we're
|
||||||
|
// opening a tab). I'm not sure.
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Allow Ghostty to execute \"\(filename)\"?"
|
||||||
|
alert.addButton(withTitle: "Allow")
|
||||||
|
alert.addButton(withTitle: "Cancel")
|
||||||
|
alert.alertStyle = .warning
|
||||||
|
switch (alert.runModal()) {
|
||||||
|
case .alertFirstButtonReturn:
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch ghostty.config.macosDockDropBehavior {
|
switch ghostty.config.macosDockDropBehavior {
|
||||||
case .new_tab: _ = TerminalController.newTab(ghostty, withBaseConfig: config)
|
case .new_tab: _ = TerminalController.newTab(ghostty, withBaseConfig: config)
|
||||||
case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config)
|
case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -424,6 +424,9 @@ extension Ghostty {
|
||||||
|
|
||||||
/// Extra input to send as stdin
|
/// Extra input to send as stdin
|
||||||
var initialInput: String? = nil
|
var initialInput: String? = nil
|
||||||
|
|
||||||
|
/// Wait after the command
|
||||||
|
var waitAfterCommand: Bool = false
|
||||||
|
|
||||||
init() {}
|
init() {}
|
||||||
|
|
||||||
|
|
@ -475,6 +478,9 @@ extension Ghostty {
|
||||||
|
|
||||||
// Zero is our default value that means to inherit the font size.
|
// Zero is our default value that means to inherit the font size.
|
||||||
config.font_size = fontSize ?? 0
|
config.font_size = fontSize ?? 0
|
||||||
|
|
||||||
|
// Set wait after command
|
||||||
|
config.wait_after_command = waitAfterCommand
|
||||||
|
|
||||||
// Use withCString to ensure strings remain valid for the duration of the closure
|
// Use withCString to ensure strings remain valid for the duration of the closure
|
||||||
return try workingDirectory.withCString { cWorkingDir in
|
return try workingDirectory.withCString { cWorkingDir in
|
||||||
|
|
|
||||||
|
|
@ -447,6 +447,9 @@ pub const Surface = struct {
|
||||||
|
|
||||||
/// Input to send to the command after it is started.
|
/// Input to send to the command after it is started.
|
||||||
initial_input: ?[*:0]const u8 = null,
|
initial_input: ?[*:0]const u8 = null,
|
||||||
|
|
||||||
|
/// Wait after the command exits
|
||||||
|
wait_after_command: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||||
|
|
@ -540,6 +543,11 @@ pub const Surface = struct {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait after command
|
||||||
|
if (opts.wait_after_command) {
|
||||||
|
config.@"wait-after-command" = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize our surface right away. We're given a view that is
|
// Initialize our surface right away. We're given a view that is
|
||||||
// ready to use.
|
// ready to use.
|
||||||
try self.core_surface.init(
|
try self.core_surface.init(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue