Join us on May 15 for a live demo of how Burp Suite DAST solves real-world security challenges.            Register Now

Drag and Pwnd: Leverage ASCII characters to exploit VS Code

Control characters like SOH, STX, EOT and ETX were never meant to run your code - but in the world of modern terminal emulators, they sometimes do. In this post, I'll dive into the forgotten mechanics of ASCII’s transport control characters, how they shaped early computing, and how they're now being abused in real-world vulnerabilities affecting modern applications.

When terminals spoke in Control Codes

Before GUI-based IDEs and fancy fonts, computers talked over serial lines - character by character. To keep those conversations structured, ASCII introduced a set of Communication Control Characters: invisible byte codes used to delimit and manage message flows between systems.

A typical message followed a strict structure:

SOH → Header → STX → Body → ETX

From transmission framing to interactive editing

Though their original purpose was communication control, many of these characters persist today - repurposed by modern software.

The Readline library (used by Bash and other interactive shells) reuses several control characters values for line editing:

There are many other Readline shortcuts (Ctr + e, Ctr + k, Ctr + l, etc.) - see the Readline man page for more. Remember that on MacOS, you need to press the Control key, not the Command key.

When the past bites back: Exploiting node-pty

Fast forward to today. Applications like Visual Studio Code use node-pty to simulate pseudo-terminals inside JavaScript environments. That forwards raw bytes directly to a shell, trusting that everything downstream will "do the right thing".

That trust breaks when control characters come into play.

Case 1: Visual Studio Code: Configuration files

In Visual Studio Code, you can define custom run configurations under Run → Add Configuration. These configurations often include an args array. During my previous research, I found an interesting variant of the OS command injection vulnerability: inserting a [ \x01 ] SOH character into arguments causes the shell to split and misinterpret them. Let's examine the following test configuration file:

{"args": ["hello","\u0001\t--args\u0001\tCalculator\u0001\t-a\u0001open\t"]}

Instead of running a Python script, Visual Studio Code opens the Calculator application on MacOS:

open -a Calculator --args cd /tmp/; /usr/bin/env /opt/homebrew/bin/python3 script.py hello

It works on Ubuntu too; use  gnome-calculator instead.

Why this works

Take a look at the VT100 User Guide, 1979 by Digital Equipment Corporation that shows how control character can be encoded using keyboard shortcuts:

As you already know, node-pty reads and sends raw bytes directly to a shell. Whenever Visual Studio Code sees byte [ \x01 ] SOH, it moves the cursor to the beginning of the line (Ctr + a).This action is repeated four times in the given example to build the payload in reverse and launch the Calculator app.

Case 2: Visual Studio Code: File Drag-and-Drop

While adding malicious arguments to the run configuration is an unusual use case, the issue can occur with other user-controlled inputs, such as filenames - anywhere node-pty blindly passes data to the shell. This is especially concerning in cases like file drag-and-drop. By default, terminal applications print the full path to the file if it was drag-and-dropped into the window. Imagine, a file with malicious payload hidden inside the name:

very very very long name \x03 open -a Calculator \x0d.txt

What the Visual Studio Code terminal sees:

'very very very long name [ Ctr + c: ignore line ]
open -a Calculator [ Enter ]
.txt'

Be aware that when a file is dragged and dropped, the carriage return character [\x0d] automatically executes a command. This prevents the user from examining the potentially harmful input in the terminal window.

Collateral damage

This vulnerability affects any operating system that permits control characters in filenames. I successfully reproduced the issue on both macOS and Ubuntu. On Windows, the risk is mitigated by two factors: the filesystem disallows control characters in filenames, and Visual Studio Code defaults to PowerShell, which does not interpret control characters as cursor movements or command breaks. Interestingly, the default macOS Terminal shows a warning when dragging files with special characters, and Ubuntu’s built-in terminal escapes control characters automatically during drag-and-drop.

Here's an escaping function that looks safe - but this technique bypasses it effortlessly.

const shellEscape = (arg: string): string => { if (/[^A-Za-z0-9_\/:=-]/.test(arg)) return arg.replace(/([$!'"();`*?{}[\]<>&%#~@\\ ])/g, '\\$1') return arg }

While I have demonstrated this vulnerability in Node.js applications using node-pty, the underlying issue lies in how applications communicate with the terminal. Any web application that blindly passes raw bytes into a terminal without proper control characters sanitization is potentially vulnerable - regardless of the underlying language, framework, or runtime environment.

Disclosure

I submitted this vulnerability to the Microsoft Security Response Center; however, they do not consider it a security issue. According to their assessment, existing mitigations - such as workspace trust warnings and the requirement for significant user interaction - lower the severity.

So, be careful next time you drag and drop a file from untrusted sources into your terminal app.

Final Thoughts

We've integrated the most effective of these techniques into the Active Scan++ extension for Burp Suite. To explore or test them yourself, simply install or update the extension directly from the Github

If you're interested in command injection research, don’t miss:

Small challenge

If you'd like to test your new knowledge, I’ve prepared a tiny Proof of Concept project for you. Your mission: read the contents of the flag.txt file located in the /app directory. Have fun!

Back to all articles

Related Research