From 5794a708ed28888bd27bc4b8f43a3071e67d3801 Mon Sep 17 00:00:00 2001 From: "ayodele.akingbule" Date: Mon, 11 Jan 2021 22:08:32 +0100 Subject: [PATCH] feat(emqx_ws_connection): Prevent EMQX from CSWSH Cross-Site Web-Socket Hijack --- etc/emqx.conf | 35 ++++++++++++++++++++++--- priv/emqx.schema | 53 +++++++++++++++++++++++++++++++++++++- src/emqx_ws_connection.erl | 32 ++++++++++++++++++++++- 3 files changed, 114 insertions(+), 6 deletions(-) diff --git a/etc/emqx.conf b/etc/emqx.conf index 0414d3c61..bf6e0470f 100644 --- a/etc/emqx.conf +++ b/etc/emqx.conf @@ -681,7 +681,7 @@ mqtt.ignore_loop_deliver = false mqtt.strict_mode = false ## Specify the response information returned to the client -## +## ## Value: String ## mqtt.response_information = example @@ -917,7 +917,7 @@ zone.external.ignore_loop_deliver = false zone.external.strict_mode = false ## Specify the response information returned to the client -## +## ## Value: String ## zone.external.response_information = example @@ -1012,7 +1012,7 @@ zone.internal.ignore_loop_deliver = false zone.internal.strict_mode = false ## Specify the response information returned to the client -## +## ## Value: String ## zone.internal.response_information = example @@ -1704,6 +1704,21 @@ listener.ws.external.nodelay = true ## Value: single | multiple listener.ws.external.mqtt_piggyback = multiple +## Enable origin check in header for websocket connection +## +## Value: true | false (default false) +listener.ws.external.check_origin_enable = false + +## Allow origin to be absent in header in websocket connection when check_origin_enable is true +## +## Value: true | false (default true) +listener.ws.external.allow_origin_absence = true + +## Comma separated list of allowed origin in header for websocket connection +## +## Value: http://url eg. local http dashboard url - http://localhost:18083, http://127.0.0.1:18083 +listener.ws.external.check_origins = http://localhost:18083, http://127.0.0.1:18083 + ##-------------------------------------------------------------------- ## External WebSocket/SSL listener for MQTT Protocol @@ -1984,6 +1999,18 @@ listener.wss.external.send_timeout_close = on ## ## Value: single | multiple listener.wss.external.mqtt_piggyback = multiple +## Enable origin check in header for secure websocket connection +## +## Value: true | false (default false) +listener.wss.external.check_origin_enable = false +## Allow origin to be absent in header in secure websocket connection when check_origin_enable is true +## +## Value: true | false (default true) +listener.wss.external.allow_origin_absence = true +## Comma separated list of allowed origin in header for secure websocket connection +## +## Value: http://url eg. https://localhost:8084, https://127.0.0.1:8084 +listener.wss.external.check_origins = https://localhost:8084, https://127.0.0.1:8084 ##-------------------------------------------------------------------- ## Modules @@ -2245,7 +2272,7 @@ alarm.actions = log,publish ## The maximum number of deactivated alarms ## -## Value: Integer +## Value: Integer ## ## Default: 1000 alarm.size_limit = 1000 diff --git a/priv/emqx.schema b/priv/emqx.schema index 8345f2f51..b6579f5fe 100644 --- a/priv/emqx.schema +++ b/priv/emqx.schema @@ -1582,6 +1582,23 @@ end}. hidden ]}. +{mapping, "listener.ws.$name.check_origin_enable", "emqx.listeners", [ + {datatype, {enum, [true, false]}}, + {default, false}, + hidden +]}. + +{mapping, "listener.ws.$name.allow_origin_absence", "emqx.listeners", [ + {datatype, {enum, [true, false]}}, + {default, true}, + hidden +]}. + +{mapping, "listener.ws.$name.check_origins", "emqx.listeners", [ + {datatype, string}, + hidden +]}. + %%-------------------------------------------------------------------- %% MQTT/WebSocket/SSL Listeners @@ -1800,6 +1817,23 @@ end}. hidden ]}. +{mapping, "listener.wss.$name.check_origin_enable", "emqx.listeners", [ + {datatype, {enum, [true, false]}}, + {default, false}, + hidden +]}. + +{mapping, "listener.wss.$name.allow_origin_absence", "emqx.listeners", [ + {datatype, {enum, [true, false]}}, + {default, true}, + hidden +]}. + +{mapping, "listener.wss.$name.check_origins", "emqx.listeners", [ + {datatype, string}, + hidden +]}. + {translation, "emqx.listeners", fun(Conf) -> Filter = fun(Opts) -> [{K, V} || {K, V} <- Opts, V =/= undefined] end, @@ -1833,6 +1867,20 @@ end}. {Limit, Duration} end, + CheckOrigin = fun(S) -> + Origins = string:tokens(S, ","), + [ list_to_binary(string:trim(O)) || O <- Origins] + end, + + WsOpts = fun(Prefix) -> + case cuttlefish_variable:filter_by_prefix(Prefix ++ ".check_origins", Conf) of + [] -> undefined; + Rules -> + OriginList = [CheckOrigin(Rule) || {_, Rule} <- Rules], + lists:flatten(OriginList) + end + end, + LisOpts = fun(Prefix) -> Filter([{acceptors, cuttlefish:conf_get(Prefix ++ ".acceptors", Conf)}, {mqtt_path, cuttlefish:conf_get(Prefix ++ ".mqtt_path", Conf, undefined)}, @@ -1849,7 +1897,10 @@ end}. {compress, cuttlefish:conf_get(Prefix ++ ".compress", Conf, undefined)}, {idle_timeout, cuttlefish:conf_get(Prefix ++ ".idle_timeout", Conf, undefined)}, {max_frame_size, cuttlefish:conf_get(Prefix ++ ".max_frame_size", Conf, undefined)}, - {mqtt_piggyback, cuttlefish:conf_get(Prefix ++ ".mqtt_piggyback", Conf, undefined)} | AccOpts(Prefix)]) + {mqtt_piggyback, cuttlefish:conf_get(Prefix ++ ".mqtt_piggyback", Conf, undefined)}, + {check_origin_enable, cuttlefish:conf_get(Prefix ++ ".check_origin_enable", Conf, undefined)}, + {allow_origin_absence, cuttlefish:conf_get(Prefix ++ ".allow_origin_absence", Conf, undefined)}, + {check_origins, WsOpts(Prefix)} | AccOpts(Prefix)]) end, DeflateOpts = fun(Prefix) -> Filter([{level, cuttlefish:conf_get(Prefix ++ ".deflate_opts.level", Conf, undefined)}, diff --git a/src/emqx_ws_connection.erl b/src/emqx_ws_connection.erl index 1fe9a105f..f016c24a0 100644 --- a/src/emqx_ws_connection.erl +++ b/src/emqx_ws_connection.erl @@ -183,18 +183,48 @@ init(Req, Opts) -> max_frame_size => MaxFrameSize, idle_timeout => IdleTimeout }, + + case check_origin_header(Req, Opts) of + {error, Message} -> + ?LOG(error, "Invalid Origin Header ~p~n", [Message]), + {ok, cowboy_req:reply(403, Req), WsOpts}; + ok -> parse_sec_websocket_protocol(Req, Opts, WsOpts) + end. + +parse_sec_websocket_protocol(Req, Opts, WsOpts) -> case cowboy_req:parse_header(<<"sec-websocket-protocol">>, Req) of undefined -> %% TODO: why not reply 500??? {cowboy_websocket, Req, [Req, Opts], WsOpts}; [<<"mqtt", Vsn/binary>>] -> Resp = cowboy_req:set_resp_header( - <<"sec-websocket-protocol">>, <<"mqtt", Vsn/binary>>, Req), + <<"sec-websocket-protocol">>, <<"mqtt", Vsn/binary>>, Req), {cowboy_websocket, Resp, [Req, Opts], WsOpts}; _ -> {ok, cowboy_req:reply(400, Req), WsOpts} end. +parse_header_fun_origin(Req, Opts) -> + case cowboy_req:header(<<"origin">>, Req) of + undefined -> + case proplists:get_value(allow_origin_absence, Opts, true) of + true -> ok; + false -> {error, origin_header_cannot_be_absent} + end; + Value -> + Origins = proplists:get_value(check_origins, Opts, []), + case lists:member(Value, Origins) of + true -> ok; + false -> {origin_not_allowed, Value} + end + end. + +check_origin_header(Req, Opts) -> + case proplists:get_value(check_origin_enable, Opts) of + true -> parse_header_fun_origin(Req, Opts); + false -> ok + end. + websocket_init([Req, Opts]) -> Peername = case proplists:get_bool(proxy_protocol, Opts) andalso maps:get(proxy_header, Req) of