All of lore.kernel.org
 help / color / mirror / Atom feed
* Security: OS command injection.
@ 2026-07-16  7:47 Sakib Sarkar
  2026-07-16 10:11 ` Alexey Gladkov
  0 siblings, 1 reply; 2+ messages in thread
From: Sakib Sarkar @ 2026-07-16  7:47 UTC (permalink / raw)
  To: gladkov.alexey@gmail.com; +Cc: kbd


[-- Attachment #1.1: Type: text/plain, Size: 121 bytes --]

Hello,
The report is a markdown file and attached.
Please let me know if more information is needed

Thanks
Sakib Sarkar

[-- Attachment #1.2: Type: text/html, Size: 1057 bytes --]

[-- Attachment #2: kbdfile-command-injection-report.md --]
[-- Type: text/markdown, Size: 3954 bytes --]

# Command Injection in libkbdfile Decompression Fallback

**CWE:** CWE-78 (OS Command Injection) 
**Component:** `src/libkbdfile/kbdfile.c` — `kbd` project (legionus/kbd)
**Affected consumers:** `loadkeys`, `setfont`, `psfxtable` 

---

## 1. Summary

`libkbdfile`, the shared file-opening layer used by `loadkeys`, `setfont`, and `psfxtable`, builds a shell command string from an unsanitized file path and executes it via `popen()` when its in-process decompression fallback is unavailable. A path containing shell metacharacters results in arbitrary command execution in the context of the process running the affected tool.

Because the vulnerable code lives in the shared library rather than in any individual binary, **every consumer of libkbdfile inherits the bug** — this is not specific to one tool's argument parsing.

---

## 2. Technical Details

### 2.1 Root cause

In `src/libkbdfile/kbdfile.c`:

- `open_pathname()` inspects the first two bytes of a file for known compression magic. If a match is found, it first tries the corresponding in-process decompressor (loaded at runtime via `dlopen` against libz/liblzma/libzstd/libbz2).
- If that decompressor is unavailable (library not installed, `dlopen` fails) and `KBDFILE_IGNORE_DECOMP_UTILS` is not set, it falls back to `pipe_open()` (kbdfile.c:149–167):

```c
static int
pipe_open(const struct decompressor *dc, struct kbdfile *fp)
{
	char *pipe_cmd;

	pipe_cmd = malloc(strlen(dc->cmd) + strlen(fp->pathname) + 2);
	if (pipe_cmd == NULL)
		return -1;

	sprintf(pipe_cmd, "%s %s", dc->cmd, fp->pathname);

	fp->fd = popen(pipe_cmd, "r");
	...
}
```

`fp->pathname` is the caller-supplied path, set directly via `kbdfile_set_pathname()` or constructed with an appended suffix (`.gz`/`.bz2`/`.xz`/`.zst`) in `maybe_pipe_open()`. It is never shell-quoted, escaped, or validated. `popen()` invokes `/bin/sh -c` on the resulting string, so shell metacharacters (`;`, `|`, `` ` ``, `$()`, etc.) in the path are interpreted by the shell rather than treated as a literal filename.

### 2.2 Trigger conditions

The vulnerable path is reached when:
1. A file's magic bytes (or suffix, via `maybe_pipe_open()`'s retry logic) match a supported compression format, **and**
2. The in-process decompressor for that format is unavailable at runtime (missing shared library dependency).

This makes the bug latent/environment-dependent: on systems where zlib/liblzma/etc. are always present, the in-process path is taken and the shell fallback is never reached. On minimal or stripped-down systems (containers, embedded images, custom installs), the fallback becomes live.

### 2.3 Proof of concept

```sh
#!/bin/sh
# PoC: command injection via unsanitized path in kbdfile pipe_open fallback
set -e
WORK="/tmp/x; touch /tmp/PWNED #"
mkdir -p "$WORK"
printf '\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff' > "$WORK/evil.gz"
loadkeys "$WORK/evil.gz" 2>/dev/null || true
[ -e /tmp/PWNED ] && echo "VULNERABLE" || echo "not triggered (in-process decompressor present)"
```

With libz absent, `loadkeys` resolves the gzip magic, the in-process decompressor returns `NULL`, and `pipe_open()` executes `gzip -d -c /tmp/x; touch /tmp/PWNED #/evil.gz` via the shell — the `;` terminates the intended command and runs the injected one.

---

## 3. Remediation

1. **Primary fix — eliminate the shell entirely.** Replace the `popen()`-based fallback with a direct `fork()`/`execvp()` (or `posix_spawn`) call using an argv array, so the path is never interpreted by `/bin/sh`:

```c
static int
pipe_open(const struct decompressor *dc, struct kbdfile *fp)
{
	int pipefd[2];
	pid_t pid;

	if (pipe(pipefd) != 0)
		return -1;

	pid = fork();
	if (pid == 0) {
		dup2(pipefd[1], STDOUT_FILENO);
		close(pipefd[0]);
		close(pipefd[1]);
		execlp(dc->cmd_token, dc->cmd_token, fp->pathname, (char *)NULL);
		_exit(127);
	}
	close(pipefd[1]);
	fp->fd = fdopen(pipefd[0], "r");
	return (fp->fd == NULL) ? -1 : 0;
}
```

^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-07-16 10:12 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-16  7:47 Security: OS command injection Sakib Sarkar
2026-07-16 10:11 ` Alexey Gladkov

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.