Skip to content

Commit 6deb263

Browse files
committed
Add check for Cross-Site Websocket Hijacking
1 parent 4ecc598 commit 6deb263

File tree

9 files changed

+212
-23
lines changed

9 files changed

+212
-23
lines changed

lib/mix/tasks/sobelow.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ defmodule Mix.Tasks.Sobelow do
5151
* Config.HTTPS
5252
* Config.HSTS
5353
* Config.Secrets
54+
* Config.CSWH
5455
* Vuln
5556
* Vuln.CookieRCE
5657
* Vuln.HeaderInject

lib/sobelow.ex

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ defmodule Sobelow do
6060
libroot_meta_files = if !phx_post_1_2?, do: get_meta_files(lib_root), else: []
6161

6262
default_router = get_router(app_name, web_root)
63-
routers = get_routers(root_meta_files ++ libroot_meta_files, default_router)
63+
64+
{routers, endpoints} =
65+
get_phoenix_files(root_meta_files ++ libroot_meta_files, default_router)
66+
6467
if Enum.empty?(routers), do: no_router()
6568

6669
FindingLog.start_link()
@@ -78,7 +81,7 @@ defmodule Sobelow do
7881
if not (format() in ["quiet", "compact", "json"]), do: IO.puts(:stderr, print_banner())
7982
Application.put_env(:sobelow, :app_name, app_name)
8083

81-
if Enum.member?(allowed, Config), do: Config.fetch(project_root, routers)
84+
if Enum.member?(allowed, Config), do: Config.fetch(project_root, routers, endpoints)
8285
if Enum.member?(allowed, Vuln), do: Vuln.get_vulns(project_root)
8386

8487
allowed = allowed -- [Config, Vuln]
@@ -271,20 +274,31 @@ defmodule Sobelow do
271274
|> Path.expand()
272275
end
273276

274-
defp get_routers(meta_files, router) do
275-
routers =
276-
Enum.flat_map(meta_files, fn meta_file ->
277-
case meta_file.is_router? do
278-
true -> [meta_file.file_path]
279-
_ -> []
277+
defp get_phoenix_files(meta_files, router) do
278+
phoenix_files =
279+
Enum.reduce(meta_files, %{routers: [], endpoints: []}, fn meta_file, acc ->
280+
cond do
281+
meta_file.is_router? ->
282+
Map.update!(acc, :routers, &[meta_file.file_path | &1])
283+
284+
meta_file.is_endpoint? ->
285+
Map.update!(acc, :endpoints, &[meta_file.file_path | &1])
286+
287+
true ->
288+
acc
280289
end
281290
end)
282291

283-
if File.exists?(router) do
284-
Enum.uniq(routers ++ [router])
285-
else
286-
routers
287-
end
292+
uniq_phoenix_files =
293+
if File.exists?(router) do
294+
Map.update!(phoenix_files, :routers, fn routers ->
295+
Enum.uniq(routers ++ [router])
296+
end)
297+
else
298+
phoenix_files
299+
end
300+
301+
{uniq_phoenix_files.routers, uniq_phoenix_files.endpoints}
288302
end
289303

290304
defp get_meta_templates(root) do
@@ -332,7 +346,8 @@ defmodule Sobelow do
332346
file_path: Path.expand(filename),
333347
def_funs: def_funs,
334348
is_controller?: Utils.is_controller?(use_funs),
335-
is_router?: Utils.is_router?(use_funs)
349+
is_router?: Utils.is_router?(use_funs),
350+
is_endpoint?: Utils.is_endpoint?(use_funs)
336351
}
337352
end
338353

@@ -478,6 +493,7 @@ defmodule Sobelow do
478493
"Config.Secrets" -> Sobelow.Config.Secrets
479494
"Config.HTTPS" -> Sobelow.Config.HTTPS
480495
"Config.HSTS" -> Sobelow.Config.HSTS
496+
"Config.CSWH" -> Sobelow.Config.CSWH
481497
"Vuln" -> Sobelow.Vuln
482498
"Vuln.CookieRCE" -> Sobelow.Vuln.CookieRCE
483499
"Vuln.HeaderInject" -> Sobelow.Vuln.HeaderInject

lib/sobelow/config.ex

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@ defmodule Sobelow.Config do
33
alias Sobelow.Config.CSRF
44
alias Sobelow.Config.CSP
55
alias Sobelow.Config.Headers
6+
alias Sobelow.Config.CSWH
67

78
@submodules [
89
Sobelow.Config.CSRF,
910
Sobelow.Config.Headers,
1011
Sobelow.Config.CSP,
1112
Sobelow.Config.Secrets,
1213
Sobelow.Config.HTTPS,
13-
Sobelow.Config.HSTS
14+
Sobelow.Config.HSTS,
15+
Sobelow.Config.CSWH
1416
]
1517

1618
use Sobelow.FindingType
1719
@skip_files ["dev.exs", "test.exs", "dev.secret.exs", "test.secret.exs"]
1820

19-
def fetch(root, router) do
21+
def fetch(root, router, endpoints) do
2022
allowed = @submodules -- Sobelow.get_ignored()
2123
ignored_files = Sobelow.get_env(:ignored_files)
2224

@@ -28,12 +30,19 @@ defmodule Sobelow.Config do
2830
|> Enum.filter(&want_to_scan?(dir_path <> &1, ignored_files))
2931

3032
Enum.each(allowed, fn mod ->
31-
if mod in [CSRF, Headers, CSP] do
32-
Enum.each(router, fn path ->
33-
apply(mod, :run, [relative_router(path, root), configs])
34-
end)
35-
else
36-
apply(mod, :run, [dir_path, configs])
33+
cond do
34+
mod in [CSRF, Headers, CSP] ->
35+
Enum.each(router, fn path ->
36+
apply(mod, :run, [relative_path(path, root), configs])
37+
end)
38+
39+
mod in [CSWH] ->
40+
Enum.each(endpoints, fn path ->
41+
apply(mod, :run, [relative_path(path, root)])
42+
end)
43+
44+
true ->
45+
apply(mod, :run, [dir_path, configs])
3746
end
3847
end)
3948
end
@@ -45,7 +54,7 @@ defmodule Sobelow.Config do
4554
do: conf
4655
end
4756

48-
defp relative_router(path, root) do
57+
defp relative_path(path, root) do
4958
path = Path.relative_to(path, Path.expand(root))
5059

5160
case Path.type(path) do

lib/sobelow/config/cswh.ex

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
defmodule Sobelow.Config.CSWH do
2+
@moduledoc """
3+
# Cross-Site Websocket Hijacking
4+
5+
Websocket connections are not bound by the same-origin policy.
6+
Connections that do not validate the origin may leak information
7+
to an attacker.
8+
9+
More information can be found here: https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html
10+
11+
Cross-Site Websocket Hijacking checks can be disabled with
12+
the following command:
13+
14+
$ mix sobelow -i Config.CSWH
15+
"""
16+
alias Sobelow.Utils
17+
use Sobelow.Finding
18+
19+
def run(endpoint) do
20+
Utils.ast(endpoint)
21+
|> Utils.get_funs_of_type(:socket)
22+
|> handle_sockets(endpoint)
23+
end
24+
25+
defp handle_sockets(sockets, endpoint) do
26+
Enum.each(sockets, fn socket ->
27+
check_socket(socket)
28+
|> add_finding(socket, endpoint)
29+
end)
30+
end
31+
32+
def check_socket({_, _, [_, _, options]}) do
33+
check_socket_options(options)
34+
end
35+
36+
def check_socket(_), do: {false, :high}
37+
38+
defp check_socket_options([{:websocket, options} | _]) when is_list(options) do
39+
case options[:check_origin] do
40+
false -> {true, :high}
41+
_ -> {true, :low}
42+
end
43+
end
44+
45+
defp check_socket_options([_ | t]), do: check_socket_options(t)
46+
defp check_socket_options([]), do: {false, :high}
47+
48+
defp add_finding(nil, _, _), do: nil
49+
defp add_finding({false, _}, _, _), do: nil
50+
51+
defp add_finding({true, confidence}, socket, endpoint) do
52+
type = "Cross-Site Websocket Hijacking"
53+
endpoint_path = "File: #{Utils.normalize_path(endpoint)}"
54+
55+
case Sobelow.format() do
56+
"json" ->
57+
finding = [
58+
type: type,
59+
endpoint: endpoint
60+
]
61+
62+
Sobelow.log_finding(finding, confidence)
63+
64+
"txt" ->
65+
Sobelow.log_finding(type, confidence)
66+
67+
Utils.print_custom_finding_metadata(
68+
socket,
69+
:highlight_all,
70+
confidence,
71+
type,
72+
[endpoint_path]
73+
)
74+
75+
"compact" ->
76+
Utils.log_compact_finding(type, confidence)
77+
78+
_ ->
79+
Sobelow.log_finding(type, confidence)
80+
end
81+
end
82+
end

lib/sobelow/utils.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ defmodule Sobelow.Utils do
6262
has_use_type?(uses, :router)
6363
end
6464

65+
def is_endpoint?([{:use, _, [{_, _, [:Phoenix, :Endpoint]}, _]} | _]), do: true
66+
def is_endpoint?([_ | t]), do: is_endpoint?(t)
67+
def is_endpoint?(_), do: false
68+
6569
def has_use_type?([{:use, _, [_, type]} | _], type), do: true
6670
def has_use_type?([_ | t], type), do: has_use_type?(t, type)
6771
def has_use_type?(_, _), do: false

test/config/cswh_test.exs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
defmodule SobelowTest.Config.CSWHTest do
2+
use ExUnit.Case
3+
alias Sobelow.Utils
4+
alias Sobelow.Config.CSWH
5+
6+
test "checks normal endpoint" do
7+
endpoint = "./test/fixtures/cswh/good_endpoint.ex"
8+
9+
is_vuln? =
10+
Utils.ast(endpoint)
11+
|> Utils.get_funs_of_type(:socket)
12+
|> Enum.any?(fn socket ->
13+
case CSWH.check_socket(socket) do
14+
{true, _} -> true
15+
_ -> false
16+
end
17+
end)
18+
19+
refute is_vuln?
20+
end
21+
22+
test "checks no-check endpoint" do
23+
endpoint = "./test/fixtures/cswh/bad_endpoint.ex"
24+
25+
is_vuln? =
26+
Utils.ast(endpoint)
27+
|> Utils.get_funs_of_type(:socket)
28+
|> Enum.any?(fn socket ->
29+
case CSWH.check_socket(socket) do
30+
{true, :high} -> true
31+
_ -> false
32+
end
33+
end)
34+
35+
assert is_vuln?
36+
end
37+
38+
test "checks loose check endpoint" do
39+
endpoint = "./test/fixtures/cswh/soso_endpoint.ex"
40+
41+
is_vuln? =
42+
Utils.ast(endpoint)
43+
|> Utils.get_funs_of_type(:socket)
44+
|> Enum.any?(fn socket ->
45+
case CSWH.check_socket(socket) do
46+
{true, :low} -> true
47+
_ -> false
48+
end
49+
end)
50+
51+
assert is_vuln?
52+
end
53+
end

test/fixtures/cswh/bad_endpoint.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule PhoenixWeb.Endpoint do
2+
use Phoenix.Endpoint, otp_app: :phoenix
3+
4+
socket("/socket", PhoenixInternalsWeb.UserSocket,
5+
websocket: [check_origin: false],
6+
longpoll: false
7+
)
8+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule PhoenixWeb.Endpoint do
2+
use Phoenix.Endpoint, otp_app: :phoenix
3+
4+
socket("/socket", PhoenixInternalsWeb.UserSocket,
5+
websocket: true,
6+
longpoll: false
7+
)
8+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule PhoenixWeb.Endpoint do
2+
use Phoenix.Endpoint, otp_app: :phoenix
3+
4+
socket("/socket", PhoenixInternalsWeb.UserSocket,
5+
websocket: [check_origin: ["//example.com"]],
6+
longpoll: false
7+
)
8+
end

0 commit comments

Comments
 (0)