fish 3.6.2 prevents external text from becoming expansion markers
CVE-2023-49284 allowed reserved Unicode markers in command-substitution output to influence shell expansion. fish 3.6.2 encodes those values safely as they enter its internal representation.
A shell needs to distinguish text produced by another program from instructions created by its own parser. fish used some Unicode non-characters as internal markers for expansion, but it allowed the same values to enter through command-substitution output. That let externally supplied data acquire a meaning reserved for the shell's own processing. fish 3.6.2, released on December 4, fixes this flaw as CVE-2023-49284.
The project's security advisory, published the same day, describes possible variable disclosure and denial of service through expansion under certain conditions. It says code execution does not appear possible. The distinction matters because this is an expansion-boundary defect with specific effects, not evidence that arbitrary command output is generally reparsed as a complete shell program.
I like fish's deliberate scripting semantics and the useful behavior it supplies interactively. Those properties rely on keeping external data separate from shell control information. An internal representation should not let an unusual character in a program's output change which variable or wildcard the shell evaluates.
The marker has meaning inside the expansion engine
The release-specific notes explain that fish uses selected Unicode non-characters to mark wildcards and expansions. Such markers can make staged processing easier by representing operations separately from their source spellings. The safety requirement is that ordinary input cannot introduce those internal operations merely by containing the same character value.
The advisory's example uses U+FDD2 followed by HOME. A vulnerable shell can interpret that combination as a variable expansion and print the home-directory value. The character itself is the relevant input. A producer does not need to emit the ordinary dollar-sign spelling for the internal marker to have an effect.
This makes command substitution a significant entry point. Its purpose is to take another command's output and supply data to the surrounding command. That output might originate in an external database or another source whose contents the shell user did not choose. Accepting the output is different from authorizing it to introduce the shell's internal expansion tokens.
The advisory demonstrates that relationship with a small Python producer. The following code emits the relevant character and a harmless variable name, matching the documented example.
print("\ufdd2HOME")
If saved as foo.py, the advisory invokes it inside command substitution in fish. The command below is a demonstration of the affected boundary, not an instruction to evaluate untrusted text as source code.
echo $(python3 foo.py)
On an affected version, the result can be the value of HOME instead of literal output from the Python process. That shows a change in meaning after the output enters fish. The report does not require an explicit eval call to demonstrate the unexpected variable expansion.
The patch repairs conversion into the internal string
The changes between 3.6.1 and 3.6.2 show a small correction in str2wcs_internal. Before the patch, conversion already treated some values specially, including the direct-encoding range and an internal separator. The new condition uses fish_reserved_codepoint to identify the broader reserved set.
When conversion encounters one of those values, it takes the direct-encoding path. The 3.6.2 conversion implementation represents input bytes through ENCODE_DIRECT_BASE and resets the multibyte conversion state as it advances. The reserved input therefore remains representable as data without being installed as the active internal marker.
The location of the fix matters. It repairs the boundary where external bytes become fish's internal wide-character representation. Adding a special case only to the variable-expansion operation would leave other reserved markers and entry paths to consider separately. A shared reserved-codepoint predicate gives the conversion code the same category that the rest of the implementation needs to protect. The distinction is between preserving the incoming bytes and granting their decoded value an internal control role. Safe encoding permits the former without allowing the latter.
The patch also checks decoded unquoted escapes. If the resulting character is reserved, that function returns failure instead of appending the character to the parsed result. The two changes address different ways the marker could enter. One encodes incoming data safely, while the other rejects a source escape that would directly create a reserved internal value.
Impact depends on where the result goes
The advisory rates the issue low and identifies user interaction and local access in its assessment. Its description keeps the effects conditional. Variable expansion can disclose information if the resulting value reaches a place where it is exposed. Large brace expansion can consume resources. Neither statement establishes that every substitution is exploitable or that every process output creates the same consequence.
The affected-version description is also qualified. The project believes the design flaw predates its version-control history and is present in all released versions over many years, with different characters in older implementations. That is a historical assessment by the maintainers. The documented patched version is 3.6.2, and the advisory lists earlier versions as affected.
I prefer the advisory's narrow account of the mechanism to a broader claim of shell-code execution. The demonstrated behavior is already a meaningful violation of the data boundary. Describing exactly how a reserved marker triggers expansion makes the fix understandable without inventing an attack capability the report does not establish.
Representation is part of the shell's semantics
The patch illustrates a constraint on internal sentinel values. Reserving a value for the implementation is useful only if conversion consistently prevents external input from producing the active sentinel. A Unicode designation alone does not make a value impossible for another program to emit, as the short Python example demonstrates.
fish's conversion code already had a way to retain bytes that could not be treated as ordinary characters. Extending that protection to every reserved codepoint preserves data while preventing it from taking the marker's operational meaning. The escape path enforces the same reservation when parsing a character escape. Together these changes keep reserved expansion markers under the control of fish's own parser and conversion rules.