quoting_style: ! in double quotes allows command injection when pasted
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 76/100
Direzione di ricerca
Inizia da src/uucore/src/lib/features/quoting_style/shell_quoter.rs, in particolare da initial_quoting() e shell_escaped_char_set(), poi leggi i test di quoting in quoting_style/mod.rs. Usa il riproduttore fornito per verificare l'output incollato e aggiungi test di regressione mirati per i nomi che contengono sia ' sia !; il lavoro è completato quando l'output può essere incollato in sicurezza in un prompt bash interattivo e i test passano.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
============================================================================
ANT-2026-WV2L2BWK [medium] CWE-116 injection
quoting_style: ! in double quotes allows command injection when pasted
Location: src/uucore/src/lib/features/quoting_style/shell_quoter.rs:234 in initial_quoting()
Commit: 7bc1eb6d1d6fa2e1dc2c9d2277cf576c05f61e47
============================================================================
A file name chosen by one user can make uutils ls print a shell-quoted form that, when another user pastes it back at an interactive bash prompt, runs a command of the attacker's choosing as that user. GNU coreutils prints a harmless form for the same name. The attached reproducer has nobody create a file in a world-writable directory; root lists it with ls on a terminal and types rm -- <what ls printed> into bash -i, and a root-owned PWNED file appears.
The quote character is chosen once, in initial_quoting() in src/uucore/src/lib/features/quoting_style/shell_quoter.rs. A name containing an apostrophe is wrapped in "…" unless it also contains a byte from ESCAPED_CHARS (line 250). That set lacks !, which interactive bash still history-expands inside double quotes. So the name x!#;touch PWNED;#' is printed as "x!#;touch PWNED;#'". At the prompt, !# re-inserts the line typed so far, including its opening ", which closes the quoted string and leaves ;touch PWNED; to run as a command. GNU 9.7 prints 'x!#;touch PWNED;#'\''' instead.
The same check serves all four shell quoting styles and every caller. This covers ls on a terminal and with the shell* styles, printf %q, stat -c %N, and the locale_aware_shell_escape() headers and diagnostics of head, tail, wc, fold and the checksum utilities. The attack needs the victim to paste the printed name into an interactive shell with history expansion on, which is bash's default. Scripts, eval and other non-interactive uses are unaffected. The payload cannot contain / or any character from ESCAPED_CHARS, but it can run a script placed next to it. The behaviour dates from 2941dfd698d1 ("ls: quoting style (#1989)") and is in every release since 0.0.5.
Details
The shell quoting styles wrap a name that contains an apostrophe in "…" instead of '…'. The list of
characters that forbids this switch has no !, and an interactive bash still performs history expansion between
double quotes. A file name can use that to close the quotes and run a command when the quoted name is entered at
a prompt:
$ touch -- 'x!#;touch PWNED;#'\''' # the name is x!#;touch PWNED;#'
$ ls # uutils, stdout is a terminal
"x!#;touch PWNED;#'"
$ rm -- "x!#;touch PWNED;#'" # pasted from the line above
rm -- "xrm -- "x;touch PWNED;#'" # bash echoes the line after history expansion
rm: cannot remove 'xrm -- x': No such file or directory
Afterwards the directory holds a new file PWNED and the original file is still there. !# expands to the
command line typed so far, here rm -- "x. That text brings a second " with it, which closes the quoted
string, so ;touch PWNED; is parsed as a command and # comments out the rest of the name.
GNU coreutils 9.7 prints 'x!#;touch PWNED;#'\''' for the same file; pasting that removes the file and does
nothing else.
The quote character is chosen once, before any character is emitted, in initial_quoting
(src/uucore/src/lib/features/quoting_style/shell_quoter.rs:227):
let has_special_chars = input.iter().any(|c| {
shell_escaped_char_set(dirname).contains(c) || (check_control_chars && c.is_ascii_control())
});
if has_special_chars {
(ShellQuotes::Single, true)
} else if input.contains(&b'\'') {
(ShellQuotes::Double, true) // line 234
and the set it consults is (shell_quoter.rs:250):
const ESCAPED_CHARS: &[u8] = b":\"`$\\^\n\t\r=";
The set has ", `, $ and \, the characters that keep a meaning between double quotes in any shell.
! keeps one too in an interactive bash, and it is missing.
The module does know ! as a special character: it is in SPECIAL_SHELL_CHARS (escaped_char.rs:12), for
which EscapedChar::new_shell returns ForceQuote (escaped_char.rs:159). But ForceQuote only sets
must_quote in push_char (shell_quoter.rs:83 and :177); the quotes value picked by initial_quoting
is never revisited. In Double mode the ! is copied to the buffer as is and finalize_shell_quoter
(shell_quoter.rs:258) wraps the buffer in ".
Without the apostrophe the same name falls through to Single, ! forces quoting, and the output
'x!#;touch PWNED;#' is inert. The apostrophe is what moves the name into double quotes.
EscapedShellQuoter::new and NonEscapedShellQuoter::new both call initial_quoting, so all four shell styles
and every utility that uses them print the same text for this name:
| command | uutils | GNU 9.7 |
|---|---|---|
ls on a terminal (ls/src/config.rs:544), ls --quoting-style=shell, shell-always, shell-escape, shell-escape-always |
"x!#;touch PWNED;#'" |
'x!#;touch PWNED;#'\''' |
printf %q NAME (uucore/src/lib/features/format/spec.rs:405) |
"x!#;touch PWNED;#'" |
'x!#;touch PWNED;#'\''' |
stat -c %N NAME (stat/src/stat.rs:1082) |
"x!#;touch PWNED;#'" |
'x!#;touch PWNED;#'\''' |
The same double-quoted form also appears in the ls -R directory headings ("./d'!x":) and in text built with
locale_aware_shell_escape() (quoting_style/mod.rs:321): the ==> name <== headers of head
(head/src/head.rs:503) and tail, and the file-name diagnostics of wc, fold and the checksum utilities
(wc: "nope'!x": No such file or directory). All of them go through the one initial_quoting check.
The injected command cannot contain a byte from ESCAPED_CHARS or, in the shell-escape styles, an ASCII
control character (either one selects single quotes), nor /. Space, ;, &, |, < and > are available,
which is enough to start a second file from the same directory: the name x!#;sh p;#' next to a script p
runs sh p. The apostrophe has to come after the payload (or appear in pairs before it): with a single
apostrophe before !#, as in it's!#;touch PWNED;#, bash stops at a continuation prompt and runs nothing.
The double-quote fallback dates from 2941dfd698d1 ("ls: quoting style (#1989)", 2021-04-01) and is present
in every release since 0.0.5.
Related finding: 133-r1h15-src_uu_ls_src_display.rs--003-shell-escape-c1-control-in-double-quotes concerns the
same double-quote fallback for names with an apostrophe, but for C1 control characters (U+0080–U+009F) in the
shell-escape styles; the two patches are independent and apply together.
Impact
ls uses the shell-escape style on a terminal so that a displayed name can be copied back into the shell.
Someone who can choose a file name that another user will list (an upload, an archive or repository, a shared or
world-writable directory) gets a command executed as that user once the user pastes the quoted name after rm,
cat, file or any other command at a bash prompt; if an administrator does this as root, the command runs as
root. The same applies to anything that types printf %q or stat %N output into an interactive shell. It
requires that step from the victim and a shell with history expansion on, which is bash's default for
interactive shells. Scripts, eval "$(printf %q …)" and other non-interactive uses do not perform history
expansion; there the double-quoted form reads back as the original name.
Short of a crafted payload, a name with both ' and ! does not read back as itself at a prompt: "a'!b"
gives bash: !b: event not found.
Reproducing
repro is a single Python script with no companion files. It uses the prebuilt /src/target/debug/coreutils
and GNU ls 9.7 from /usr/bin as the reference.
It runs the same scenario twice, first with GNU ls (control) and then with uutils ls:
- Setup. It creates a temporary directory with mode
1777, which stands in for a shared directory such as/tmp. - Attacker. The user
nobodycreates one empty file there namedx!#;touch PWNED;#'. Making that
file is all the attacker does. - Victim (root). Root runs plain
lsin the directory with a pseudo-terminal as stdout, which is what makes
lschoose shell-escape quoting. It then typesrm -- <the text ls printed>intobash --norc --noprofile -i
in the same directory. History expansion is on by default in an interactive bash. bash reads the line from a
pipe so the script can supply it, so it prints two job-control warnings at startup. Those warnings are expected.
For each run the script prints what ls showed, the line root typed, everything bash printed, and every file left
in the directory with its owner. Expected output:
CONTROL GNU ls:lsshows'x!#;touch PWNED;#'\''',rmremoves the file and the directory ends up
empty ({}).OBSERVED uutils ls:lsshows"x!#;touch PWNED;#'". bash echoes the history-expanded line
rm -- "xrm -- "x;touch PWNED;#'", andrmfails onxrm -- x. The directory then holds
{'PWNED': 'root', "x!#;touch PWNED;#'": 'nobody'}, so the attacker'stouch PWNEDran as root and the
file is still there.
The last line is BUG only if the GNU directory is empty, which shows that pasting a correctly quoted name
removes the file and runs nothing else, and the uutils directory contains a PWNED owned by root. Otherwise it is
OK. The temporary directories are removed when the script finishes.
The pipe is only a convenience of the script. Typing the same line at a bash -i prompt on a pseudo-terminal
(readline, histexpand at its default) gives the same result: bash echoes the expanded line and the embedded
command runs as the user at the prompt.
Suggested fix
The attached patch adds ! to ESCAPED_CHARS, so a name that contains it always gets single quotes:
--- a/src/uucore/src/lib/features/quoting_style/shell_quoter.rs
+++ b/src/uucore/src/lib/features/quoting_style/shell_quoter.rs
@@ -250 +250 @@ fn shell_escaped_char_set(is_dirname: bool) -> &'static [u8] {
- const ESCAPED_CHARS: &[u8] = b":\"`$\\^\n\t\r=";
+ const ESCAPED_CHARS: &[u8] = b":\"`$\\^\n\t\r=!";
! has to go at the end of the list: shell_escaped_char_set drops the first byte (:) for non-directory
names by slicing.
Membership in this set also sets must_quote, which ! already does through ForceQuote, so output changes
only for names that contain both ' and !, and it becomes what GNU prints. Since the quote character is
chosen in this one place, the change covers all four shell styles and every caller: ls, printf %q and
stat -c %N print 'x!#;touch PWNED;#'\''', and the reproducer prints OK on a rebuilt clean checkout.
a'!b becomes 'a'\''!b', while can't ("can't") and a!b ('a!b') are printed as before; all match
GNU 9.7.
Besides the one-byte change, the patch adds tests: a'!b assertions for initial_quoting, a can'!t case
in test_special_chars_in_double_quotes (quoting_style/mod.rs), and one integration test each in
tests/by-util/test_ls.rs and tests/by-util/test_printf.rs.
GNU also keeps single quotes when any of # & ( ) * ; < > ? [ { | } ~ appears next to the apostrophe
("a';b" here, 'a'\'';b' in GNU). Those characters are inert between double quotes, so that is a
compatibility difference only. #, ~, { and } do not force quoting when they appear alone inside a name
(a#b is printed bare by both), so they cannot be added to the same set without changing that.
Diagnostics built with quote() from the third-party os_display crate make the same choice
(rm: cannot remove "nope'!x": No such file or directory, where GNU prints 'nope'\''!x'). That decision is
made in the external crate, whose DOUBLE_UNSAFE list also lacks !, and is not changed by this patch; it
needs a separate fix in os_display.
Running the reproducer
ANT-2026-WV2L2BWK/repro, next to this file. It was written by an automated agent and our check ran it as root in a privileged container: run it INSIDE A THROWAWAY VM, with the image oss-scan-7bc1eb6d1d6f built by bash build/build-image.sh, from the archive's top directory:
docker run --rm --network=none --privileged -u 0 -w /src -v "$PWD/ANT-2026-WV2L2BWK/repro:/tmp/repro:ro" oss-scan-7bc1eb6d1d6f /tmp/repro
It prints its evidence; the last line is BUG if the issue triggered, OK otherwise.
- Lingua principale
- Rust
- Stelle
- 24.3k
- Fork
- 2.1k
- Merge medio
- 2g 37m
- PR unite (30g)
- 336
Preparare l'ambiente
Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di uutils/coreutils
-
split: -b and -l ignore short writes, exit 0 with a truncated last chunkForse già presa @aj-codespy l’ha presa oggi. Apertagood first issue I - Incorrect oss-scanner U - split
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
uutils/coreutils#15820 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
df: prints raw control characters in file and mount names, unlike GNUForse già presa @aj-codespy l’ha presa oggi. Apertagood first issue I - Incorrect oss-scanner U - df
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
uutils/coreutils#15817 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
good first issue I - Incorrect oss-scanner U - groups U - pinky
Difficoltà 2/5 1-3 ore Idoneità per principianti 83/100
I maintainer di solito rispondono entro 1 giorno
-
good first issue I - Incorrect oss-scanner U - All (uucore) U - rm
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
I maintainer di solito rispondono entro 1 giorno
-
good first issue I - Incorrect oss-scanner U - All (uucore) U - ls
Difficoltà 2/5 1-3 ore Idoneità per principianti 79/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di uutils/coreutils
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
manuscript-lang/manuscript#1364 ·
I maintainer di solito rispondono entro 1 giorno
-
`printf "%o"` prints decimalAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
fable-compiler/Fable#5157 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 73/100
I maintainer di solito rispondono entro 1 giorno
-
C-bug needs-triage O-UEFI T-libs
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
移动端宽屏适配Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 64/100
I maintainer di solito rispondono entro 1 giorno