Skip to content

Commit 80de18a

Browse files
whatyouhideessen
authored andcommitted
Fix WebSocket UTF-8 validation crash
Invalid UTF-8 text payloads could crash continuation states in the WebSocket UTF-8 decoder when an ASCII byte appeared where a continuation byte was expected. Return the UTF-8 error state instead so text, close and fragmented payloads fail as badencoding.
1 parent d22dddf commit 80de18a

1 file changed

Lines changed: 93 additions & 14 deletions

File tree

src/cow_ws.erl

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ validate_s0(<<C,R/bits>>) when C >= 128 ->
692692
validate_s0(Text) ->
693693
validate_ascii(Text).
694694

695-
validate_s2(<<C,R/bits>>) ->
695+
validate_s2(<<C,R/bits>>) when C >= 128 ->
696696
Class = element(C - 127, utf8_class()),
697697
case Class of
698698
7 -> validate_s0(R);
@@ -701,9 +701,11 @@ validate_s2(<<C,R/bits>>) ->
701701
_ -> 1
702702
end;
703703
validate_s2(<<>>) ->
704-
2.
704+
2;
705+
validate_s2(_) ->
706+
1.
705707

706-
validate_s3(<<C,R/bits>>) ->
708+
validate_s3(<<C,R/bits>>) when C >= 128 ->
707709
Class = element(C - 127, utf8_class()),
708710
case Class of
709711
7 -> validate_s2(R);
@@ -712,38 +714,46 @@ validate_s3(<<C,R/bits>>) ->
712714
_ -> 1
713715
end;
714716
validate_s3(<<>>) ->
715-
3.
717+
3;
718+
validate_s3(_) ->
719+
1.
716720

717-
validate_s4(<<C,R/bits>>) ->
721+
validate_s4(<<C,R/bits>>) when C >= 128 ->
718722
Class = element(C - 127, utf8_class()),
719723
case Class of
720724
7 -> validate_s2(R);
721725
_ -> 1
722726
end;
723727
validate_s4(<<>>) ->
724-
4.
728+
4;
729+
validate_s4(_) ->
730+
1.
725731

726-
validate_s5(<<C,R/bits>>) ->
732+
validate_s5(<<C,R/bits>>) when C >= 128 ->
727733
Class = element(C - 127, utf8_class()),
728734
case Class of
729735
1 -> validate_s2(R);
730736
9 -> validate_s2(R);
731737
_ -> 1
732738
end;
733739
validate_s5(<<>>) ->
734-
5.
740+
5;
741+
validate_s5(_) ->
742+
1.
735743

736-
validate_s6(<<C,R/bits>>) ->
744+
validate_s6(<<C,R/bits>>) when C >= 128 ->
737745
Class = element(C - 127, utf8_class()),
738746
case Class of
739747
7 -> validate_s3(R);
740748
9 -> validate_s3(R);
741749
_ -> 1
742750
end;
743751
validate_s6(<<>>) ->
744-
6.
752+
6;
753+
validate_s6(_) ->
754+
1.
745755

746-
validate_s7(<<C,R/bits>>) ->
756+
validate_s7(<<C,R/bits>>) when C >= 128 ->
747757
Class = element(C - 127, utf8_class()),
748758
case Class of
749759
7 -> validate_s3(R);
@@ -752,16 +762,20 @@ validate_s7(<<C,R/bits>>) ->
752762
_ -> 1
753763
end;
754764
validate_s7(<<>>) ->
755-
7.
765+
7;
766+
validate_s7(_) ->
767+
1.
756768

757-
validate_s8(<<C,R/bits>>) ->
769+
validate_s8(<<C,R/bits>>) when C >= 128 ->
758770
Class = element(C - 127, utf8_class()),
759771
case Class of
760772
1 -> validate_s3(R);
761773
_ -> 1
762774
end;
763775
validate_s8(<<>>) ->
764-
8.
776+
8;
777+
validate_s8(_) ->
778+
1.
765779

766780
utf8_class() ->
767781
{
@@ -775,6 +789,71 @@ utf8_class() ->
775789
11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8
776790
}.
777791

792+
-ifdef(TEST).
793+
794+
validate_text_valid_test() ->
795+
0 = validate_text(<<>>, 0),
796+
0 = validate_text(<<"Hello world!">>, 0),
797+
0 = validate_text(<<16#c2, 16#a2>>, 0),
798+
0 = validate_text(<<16#e2, 16#82, 16#ac>>, 0),
799+
0 = validate_text(<<16#f0, 16#9f, 16#98, 16#80>>, 0),
800+
2 = validate_text(<<16#c2>>, 0),
801+
0 = validate_text(<<16#a2>>, 2),
802+
ok.
803+
804+
validate_text_rejects_ascii_after_multibyte_lead_test_() ->
805+
Tests = [
806+
{<<"s2">>, <<16#c2, $A>>},
807+
{<<"s3">>, <<16#e1, $A>>},
808+
{<<"s4">>, <<16#e0, $A>>},
809+
{<<"s5">>, <<16#ed, $A>>},
810+
{<<"s6">>, <<16#f0, $A>>},
811+
{<<"s7">>, <<16#f1, $A>>},
812+
{<<"s8">>, <<16#f4, $A>>}
813+
],
814+
[{Name, fun() -> 1 = validate_text(Text, 0) end}
815+
|| {Name, Text} <- Tests].
816+
817+
validate_text_rejects_streamed_ascii_in_continuation_state_test_() ->
818+
Tests = [
819+
{<<"s2">>, 2},
820+
{<<"s3">>, 3},
821+
{<<"s4">>, 4},
822+
{<<"s5">>, 5},
823+
{<<"s6">>, 6},
824+
{<<"s7">>, 7},
825+
{<<"s8">>, 8}
826+
],
827+
[{Name, fun() -> 1 = validate_text(<<"A">>, State) end}
828+
|| {Name, State} <- Tests].
829+
830+
parse_payload_text_utf8_test() ->
831+
Payload = <<16#f0, 16#9f, 16#98, 16#80>>,
832+
{ok, Payload, 0, <<>>}
833+
= parse_payload(Payload, undefined, 0, 0, text, 4, undefined, #{},
834+
<<0:3>>),
835+
ok.
836+
837+
parse_payload_rejects_invalid_utf8_text_test() ->
838+
{error, badencoding}
839+
= parse_payload(<<16#c2, $A>>, undefined, 0, 0, text, 2,
840+
undefined, #{}, <<0:3>>),
841+
ok.
842+
843+
parse_payload_rejects_invalid_utf8_close_test() ->
844+
{error, badencoding}
845+
= parse_payload(<<1000:16, 16#c2, $A>>, undefined, 0, 0,
846+
close, 4, undefined, #{}, <<0:3>>),
847+
ok.
848+
849+
parse_payload_rejects_streamed_invalid_utf8_fragment_test() ->
850+
{error, badencoding}
851+
= parse_payload(<<"A">>, undefined, 2, 0, fragment, 1,
852+
{fin, text, <<0:3>>}, #{}, <<0:3>>),
853+
ok.
854+
855+
-endif.
856+
778857
%% @doc Return a frame tuple from parsed state and data.
779858

780859
-spec make_frame(frame_type(), binary(), close_code(), frag_state()) -> frame().

0 commit comments

Comments
 (0)