Skip to content

Commit 088e58c

Browse files
committed
stdlib: Escape C1 control characters in bwrite_string unicode path
string_bin_escape_unicode/6 passed all bytes > 127 through without checking for C1 control characters (U+0080-U+009F). These encode as 0xC2 0x80-0x9F in UTF-8 and could enable terminal control injection when rendering untrusted strings. Fix by matching the specific two-byte C1 sequences and octal-escaping them, consistent with the list-based write_string path.
1 parent f8d1d80 commit 088e58c

2 files changed

Lines changed: 28 additions & 0 deletions

File tree

lib/stdlib/src/io_lib.erl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,22 @@ string_bin_escape_latin1(_, Orig, _, Acc, Skip, Len) ->
10841084
false -> [Acc | binary_part(Orig, Skip, Len)]
10851085
end.
10861086

1087+
string_bin_escape_unicode(<<16#C2, Byte, Rest/binary>>, Orig, Q, Acc, Skip0, Len)
1088+
when Byte >= 16#80, Byte =< 16#9F ->
1089+
%% C1 control characters (U+0080-U+009F) - octal escape to match list path.
1090+
C1 = (Byte bsr 6) + $0,
1091+
C2 = ((Byte bsr 3) band 7) + $0,
1092+
C3 = (Byte band 7) + $0,
1093+
Escape = [$\\,C1,C2,C3],
1094+
case Len =:= 0 of
1095+
true ->
1096+
Skip = Skip0 + 2,
1097+
string_bin_escape_unicode(Rest, Orig, Q, [Acc | Escape], Skip, 0);
1098+
false ->
1099+
Skip = Skip0 + Len + 2,
1100+
Part = binary_part(Orig, Skip0, Len),
1101+
string_bin_escape_unicode(Rest, Orig, Q, [Acc, Part | Escape], Skip, 0)
1102+
end;
10871103
string_bin_escape_unicode(<<Byte, Rest/binary>>, Orig, Q, Acc, Skip0, Len) when Byte > 127 ->
10881104
string_bin_escape_unicode(Rest, Orig, Q, Acc, Skip0, Len+1);
10891105
string_bin_escape_unicode(<<Byte, Rest/binary>>, Orig, Q, Acc, Skip0, Len) ->

lib/stdlib/test/io_SUITE.erl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2260,6 +2260,18 @@ otp_10302(Suite) when is_list(Suite) ->
22602260
not is_latin1(S)],
22612261
L2 = lists:seq(256, 512),
22622262

2263+
%% C1 controls (U+0080-U+009F) must be escaped in binary unicode path.
2264+
lists:foreach(
2265+
fun(C) ->
2266+
Bin = <<C/utf8>>,
2267+
Escaped = io_lib:bwrite_string(Bin, $", unicode),
2268+
%% Must not contain raw C1 bytes - should be octal-escaped.
2269+
nomatch = binary:match(Escaped, Bin),
2270+
%% Verify matches list path.
2271+
ListEsc = lists:flatten(io_lib:write_string([C])),
2272+
ListEsc = unicode:characters_to_list(Escaped)
2273+
end, lists:seq(16#80, 16#9F)),
2274+
22632275
ok.
22642276

22652277
pretty(Term, Depth) when is_integer(Depth) ->

0 commit comments

Comments
 (0)