Back to Blog
critical SEVERITY4 min read

Tauri type_text() Command Injection via Control Characters in

The type_text() system command handler in Tauri accepted arbitrary text up to 2000 UTF-16 characters without validating content, allowing injection of control characters that SendInput's Unicode path interprets as real key presses. An attacker could inject newline, escape, or tab characters to trigger actions in the focused window beyond mere text typing.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 24, 2026•Reviewed September 24, 2026

Answer Summary

The type_text() public API in Tauri-based applications accepted unvalidated text input passed directly to Windows SendInput. An attacker could inject control characters like newline or escape that SendInput interprets as actual key presses, triggering unintended actions in whatever window had focus. The fix filters all control characters using chars().filter(|c| !c.is_control()) before encoding to UTF-16. CWE-78 (OS Command Injection).

Vulnerability at a Glance

cweCWE-78 (OS Command Injection)
fixFilter control characters with is_control() before encoding to UTF-16 for SendInput
riskArbitrary key injection in focused applications, potentially triggering dangerous shortcuts or UI actions
languageRust
root causeUnvalidated text passed to SendInput with KEYEVENTF_UNICODE, where control characters become actual key presses
vulnerabilityCommand injection via keyboard input control characters

Affected Versions

Affected not applicable (first-party code)
Fixed in not applicable (first-party code) — see PR for fix commit
Ecosystem N/A
CVE / GHSA not assigned
CWE CWE-78 (OS Command Injection)

The Vulnerability Explained

The type_text() function exposes Windows SendInput functionality to Tauri command handlers, accepting arbitrary text input that it translates into simulated keyboard events. The vulnerable code passed this text directly through without examining its contents:

fn type_text(text: &str) -> Result<(), String> {
    // ...
    let mut inputs: Vec<INPUT> = Vec::new();
    for unit in text.encode_utf16().take(2000) {
        for up in [false, true] {
            let mut flags = KEYEVENTF_UNICODE;
            // ...

The critical issue: KEYEVENTF_UNICODE tells SendInput to treat the input as Unicode character codes, but Windows still interprets certain control characters (U+000D carriage return, U+000A line feed, U+0009 horizontal tab, U+001B escape, and others) as semantic key events. When type_text() receives a string like "malicious\n", that newline becomes an actual Enter key press in whatever window currently has focus—not a visible newline character, but the form submission, dialog confirmation, or command execution that Enter typically triggers.

An attacker controlling the text parameter could craft payloads like:
- "\x1b" — injects Escape, potentially dismissing security prompts or canceling operations
- "\t\n" — Tab followed by Enter, navigating to a default button and activating it
- "text\x0d\x0a" — Carriage return + line feed submitting whatever form or dialog is active

This transforms a "type text" capability into an arbitrary key press injection primitive, with consequences depending entirely on what application happens to be focused at the moment of injection.

The Fix

The remediation adds explicit control character filtering before the UTF-16 encoding:

    // Strip control characters (CR/LF/TAB/ESC/etc.): SendInput's Unicode path can have
    // these interpreted as real key presses (e.g. Enter/Escape) by the focused window,
    // letting injected text trigger actions instead of merely typing printable content.
    let safe_text: String = text.chars().filter(|c| !c.is_control()).collect();
    let mut inputs: Vec<INPUT> = Vec::new();
    for unit in safe_text.encode_utf16().take(2000) {

Before: text.encode_utf16() — raw input encoded directly, control characters become key events

After: text.chars().filter(|c| !c.is_control()).collect().encode_utf16() — only non-control characters proceed

The is_control() method in Rust's standard library returns true for all Unicode characters with the General Category "Cc" (Other, Control), which includes ASCII control characters 0x00–0x1F and 0x7F, plus C1 control codes 0x80–0x9F. This precisely targets the characters that SendInput interprets semantically rather than literally.

The fix maintains support for international text, emoji, and other Unicode content while removing the dangerous injection vector. The 2000-character limit remains as a defense-in-depth measure against excessive input, but the control character filter is what eliminates the CWE-78 violation.

Key Takeaways

  • SendInput with KEYEVENTF_UNICODE is not safe for untrusted text — the Unicode path still processes control characters as key events, not literal glyphs
  • Rust's is_control() precisely matches the dangerous character set — no need for manual ASCII range checks or regex patterns when the standard library provides semantic categorization
  • Input validation must happen before encoding — filtering after UTF-16 conversion would miss multi-byte control sequences and complicate the logic
  • "Type text" APIs are command injection surfaces — any interface that simulates keyboard input carries the risk of triggering application shortcuts, form submissions, or security dialogs through carefully crafted character sequences

How Orbis AppSec Detected This

Source: The text parameter passed to the type_text() Tauri command handler

Sink: SendInput Windows API invoked with KEYEVENTF_UNICODE flag, where control characters in the input array generate semantic key events

Missing control: No validation that input characters were printable before encoding to UTF-16; is_control() check absent

CWE: CWE-78 (OS Command Injection) — injection of special characters that alter the interpretation of a system command (here, the "command" being the sequence of key events)

Fix: Filter all control characters using chars().filter(|c| !c.is_control()) before UTF-16 encoding, ensuring only printable content reaches SendInput

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This vulnerability demonstrates how even seemingly simple "type this text" functionality can become a command injection vector when the underlying platform interprets certain byte values as control signals. The Windows SendInput API's Unicode mode doesn't provide the isolation one might expect—control characters retain their semantic meaning and become actual key presses in the target application.

For Tauri developers and anyone building automation interfaces, the lesson is clear: validate early, use semantic character properties rather than manual range checks, and never assume that "Unicode" means "safe." The is_control() method provides exactly the right abstraction for this case, filtering the General Category Cc characters that trigger unwanted behavior while preserving the international text support that makes Unicode valuable in the first place.

Prevention and further reading

Frequently Asked Questions

Does the 2000-character limit in type_text() prevent exploitation of this vulnerability?

No. The limit only restricts total input length; a single control character anywhere in those 2000 UTF-16 units is sufficient to trigger unintended actions, and the vulnerability exists regardless of how much text surrounds it.

Why does KEYEVENTF_UNICODE with SendInput interpret control characters as key presses rather than literal Unicode code points?

SendInput's Unicode path still processes certain control characters (CR, LF, TAB, ESC) as semantic key events that the receiving window interprets according to its own logic—Enter submits forms, Escape cancels dialogs—rather than displaying them as visible glyphs.

Is filtering with is_control() sufficient, or should type_text() also restrict to printable ASCII?

is_control() is sufficient for this specific vulnerability because it removes all characters with the Unicode General Category Cc (control), which includes the problematic CR/LF/TAB/ESC bytes. However, applications with stricter requirements may additionally want to validate against a printable allowlist depending on their threat model.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1

Related Articles

critical

webapp-testing Server Script: Shell Injection via Unquoted Command

A critical command injection vulnerability in a web server testing utility allowed attackers to execute arbitrary shell commands by injecting metacharacters into command-line arguments. The fix removes shell interpretation and safely tokenizes user-supplied commands using `shlex.split()`, while preserving support for legitimate directory-change workflows.

high

THiNX Device Platform: Command Injection via source.owner in Git Fetch

The THiNX device management platform fixed a critical vulnerability where the `source.owner` parameter flowed directly into shell commands during git repository operations. An attacker with source configuration access could inject arbitrary shell commands by providing malicious owner values containing metacharacters like `; curl`.

critical

backtest.js CLI args: Unvalidated --days and --start Options

The backtest.js command-line tool accepted --days and --start arguments without validation, creating a command injection vector when the script is invoked by upstream processes with untrusted input. The fix adds strict input validation: --days must be a positive integer ≤ 3650, and --start must match the YYYY-MM-DD date format.

high

runStreaming() Command Injection: Defense-in-Depth for Electron Child

An Electron application's `runStreaming()` utility accepted a command string and argument array without validating either, creating a latent command injection vector. The fix adds strict type checking and a whitelist regex that rejects shell metacharacters, bounding the failure mode even if caller input becomes attacker-influenced.

critical

docker_rpc.uc Command Injection: Unsanitized RPC Parameters

A critical command injection vulnerability in the Docker RPC handler allowed authenticated attackers to execute arbitrary system commands by injecting shell metacharacters into container ID, port, user ID, or command parameters. The fix validates all user-supplied inputs against strict whitelist patterns before interpolating them into shell commands.

critical

JdbcSinkFunction.invoke() SQL Injection via Unvalidated Identifiers

The `JdbcSinkFunction.invoke()` method in Lacus's RTC engine built SQL INSERT statements with `String.format()`, placing database name, table name, and column names directly into the query string without validation. Although the actual row values used parameterized `?` placeholders, the identifiers themselves were injectable, letting a crafted sink configuration break out of the intended INSERT statement. The fix adds a strict allowlist regex that rejects any identifier containing characters out