Skip to content

Commit b591ffa

Browse files
Merge pull request #87 from getsentry/default-scrubbers
Default scrubbers
2 parents 7f1480f + 9141d66 commit b591ffa

File tree

7 files changed

+122
-31
lines changed

7 files changed

+122
-31
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44

55
* Enhancements
66
* Return a task when sending a Sentry event
7+
* Provide default scrubber for request body and headers (`Sentry.Plug.default_body_scrubber` and `Sentry.Plug.default_header_scrubber`)
8+
* Header scrubbing can now be configured with `:header_scrubber`
79

810
* Bug Fixes
911
* Ensure `mix sentry.send_test_event` finishes sending event before ending Mix task
1012

1113
* Backward incompatible changes
1214
* `Sentry.capture_exception/1` now returns a `Task` instead of `{:ok, PID}`
15+
* Sentry.Plug `:scrubber` option has been removed in favor of the more descriptive `:body_scrubber`option, which defaults to newly added `Sentry.Plug.default_scrubber/1`
16+
* New option for Sentry.Plug `:header_scrubber` defaults to newly added `Sentry.Plug.default_header_scrubber/1`
17+
* Request bodies were not previously sent by default. Because of above change, request bodies are now sent by default after being scrubbed by default scrubber. To prevent sending any data, `:body_scrubber` should be set to `nil`

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ To use Sentry with your projects, edit your mix.exs file to add it as a dependen
1313

1414
```elixir
1515
defp application do
16-
[applications: [:sentry, :logger]]
16+
[applications: [:sentry, :logger]]
1717
end
1818

1919
defp deps do

docs/config.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
Configuration
22
=============
33

4-
Configuration is handled using the standard elixir configuration.
4+
Configuration is handled using the standard Elixir configuration.
55

6-
Simply add configuration to the `:sentry` key in the file `config/prod.exs`:
6+
Simply add configuration to the ``:sentry`` key in the file ``config/prod.exs``:
77

88
.. code-block:: elixir
99

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ Want more? Have a look at the full documentation for more information.
118118

119119
usage
120120
config
121+
plug
121122

122123
Resources:
123124

docs/plug.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Sentry.Plug
2+
=============
3+
4+
Sentry.Plug provides basic funcitonality to handle Plug.ErrorHandler.
5+
6+
To capture errors, simply put the following in your router:
7+
8+
.. code-block:: elixir
9+
10+
use Sentry.Plug
11+
12+
Optional settings
13+
------------------
14+
15+
.. describe:: body_scrubber
16+
17+
The function to call before sending the body of the request to Sentry. It will default to ``Sentry.Plug.default_body_scrubber/1``, which will remove sensitive parameters like "password", "passwd", "secret", or any values resembling a credit card.
18+
19+
.. describe:: header_scrubber
20+
21+
The function to call before sending the headers of the request to Sentry. It will default to ``Sentry.Plug.default_header_scrubber/1``, which will remove "Authorization" and "Authentication" headers.
22+
23+
.. describe:: request_id_header
24+
25+
If you're using Phoenix, Plug.RequestId, or another method to set a request ID response header, and would like to include that information with errors reported by Sentry.Plug, the `:request_id_header` option allows you to set which header key Sentry should check. It will default to "x-request-id", which Plug.RequestId (and therefore Phoenix) also default to.

lib/sentry/plug.ex

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
defmodule Sentry.Plug do
2+
@default_scrubbed_param_keys ["password", "passwd", "secret"]
3+
@default_scrubbed_header_keys ["authorization", "authentication"]
4+
@credit_card_regex ~r/^(?:\d[ -]*?){13,16}$/
5+
@scrubbed_value "*********"
6+
27
@moduledoc """
38
Provides basic funcitonality to handle Plug.ErrorHandler
49
@@ -11,33 +16,53 @@ defmodule Sentry.Plug do
1116
1217
### Sending Post Body Params
1318
14-
In order to send post body parameters you need to first scrub them of sensitive information. To
15-
do so we ask you to pass a `scrubber` key which accepts a `Plug.Conn` and returns a map with keys
16-
to send.
19+
In order to send post body parameters you should first scrub them of sensitive information. By default,
20+
they will be scrubbed with `Sentry.Plug.default_body_scrubber/1`. It can be overridden by passing
21+
the `body_scrubber` option, which accepts a `Plug.Conn` and returns a map to send. Setting `:body_scrubber` to nil
22+
will not send any data back. If you would like to make use of Sentry's default scrubber behavior in a custom scrubber,
23+
it can be called directly. An example configuration may look like the following:
1724
1825
def scrub_params(conn) do
19-
conn.params # Make sure the params have been fetched.
20-
|> Map.to_list
21-
|> Enum.filter(fn ({key, val}) ->
22-
key in ~w(password passwd secret credit_card) ||
23-
Regex.match?(~r/^(?:\d[ -]*?){13,16}$r/, val) # Matches Credit Cards
24-
end)
25-
|> Enum.into(%{})
26+
# Makes use of the default body_scrubber to avoid sending password and credit card information in plain text.
27+
# To also prevent sending our sensitive "my_secret_field" and "other_sensitive_data" fields, we simply drop those keys.
28+
Sentry.Plug.default_body_scrubber(conn)
29+
|> Map.drop(["my_secret_field", "other_sensitive_data"])
2630
end
2731
28-
Then pass it into Sentry.Plug
32+
Then pass it into Sentry.Plug:
2933
30-
use Sentry.Plug, scrubber: &scrub_params/1
34+
use Sentry.Plug, body_scrubber: &scrub_params/1
3135
32-
You can also pass it in as a `{module, fun}` like so
36+
You can also pass it in as a `{module, fun}` like so:
3337
34-
use Sentry.Plug, scrubber: {MyModule, :scrub_params}
38+
use Sentry.Plug, body_scrubber: {MyModule, :scrub_params}
3539
3640
*Please Note*: If you are sending large files you will want to scrub them out.
3741
3842
### Headers Scrubber
3943
40-
By default we will scrub Authorization and Authentication headers from all requests before sending them.
44+
By default Sentry will scrub Authorization and Authentication headers from all requests before sending them. It can be
45+
configured similarly to the body params scrubber, but is configured with the `:header_scrubber` key.
46+
47+
def scrub_headers(conn) do
48+
# default is: Sentry.Plug.default_header_scrubber(conn)
49+
#
50+
# We do not want to include Content-Type or User-Agent in reported headers, so we drop them.
51+
Enum.into(conn.req_headers, %{})
52+
|> Map.drop(["content-type", "user-agent"])
53+
end
54+
55+
Then pass it into Sentry.Plug:
56+
57+
use Sentry.Plug, header_scrubber: &scrub_headers/1
58+
59+
It can also be passed in as a `{module, fun}` like so:
60+
61+
use Sentry.Plug, header_scrubber: {MyModule, :scrub_headers}
62+
63+
To configure scrubbing body and header data, we can set both configuration keys:
64+
65+
use Sentry.Plug, header_scrubber: &scrub_headers/1, body_scrubber: &scrub_params/1
4166
4267
### Including Request Identifiers
4368
@@ -50,21 +75,24 @@ defmodule Sentry.Plug do
5075

5176

5277
defmacro __using__(env) do
53-
scrubber = Keyword.get(env, :scrubber, nil)
78+
body_scrubber = Keyword.get(env, :body_scrubber, {__MODULE__, :default_body_scrubber})
79+
header_scrubber = Keyword.get(env, :header_scrubber, {__MODULE__, :default_header_scrubber})
5480
request_id_header = Keyword.get(env, :request_id_header, nil)
5581

5682
quote do
5783
defp handle_errors(conn, %{kind: kind, reason: reason, stack: stack}) do
58-
opts = [scrubber: unquote(scrubber), request_id_header: unquote(request_id_header)]
84+
opts = [body_scrubber: unquote(body_scrubber), header_scrubber: unquote(header_scrubber),
85+
request_id_header: unquote(request_id_header)]
5986
request = Sentry.Plug.build_request_interface_data(conn, opts)
6087
exception = Exception.normalize(kind, reason, stack)
6188
Sentry.capture_exception(exception, [stacktrace: stack, request: request])
6289
end
6390
end
6491
end
6592

66-
def build_request_interface_data(%{__struct__: Plug.Conn} = conn, opts) do
67-
scrubber = Keyword.get(opts, :scrubber)
93+
def build_request_interface_data(%Plug.Conn{} = conn, opts) do
94+
body_scrubber = Keyword.get(opts, :body_scrubber)
95+
header_scrubber = Keyword.get(opts, :header_scrubber)
6896
request_id = Keyword.get(opts, :request_id_header) || @default_plug_request_id_header
6997

7098
conn = conn
@@ -74,10 +102,10 @@ defmodule Sentry.Plug do
74102
%{
75103
url: "#{conn.scheme}://#{conn.host}:#{conn.port}#{conn.request_path}",
76104
method: conn.method,
77-
data: handle_request_data(conn, scrubber),
105+
data: handle_data(conn, body_scrubber),
78106
query_string: conn.query_string,
79107
cookies: conn.req_cookies,
80-
headers: Enum.into(conn.req_headers, %{}) |> scrub_headers(),
108+
headers: handle_data(conn, header_scrubber),
81109
env: %{
82110
"REMOTE_ADDR" => remote_address(conn.remote_ip),
83111
"REMOTE_PORT" => remote_port(conn.peer),
@@ -96,17 +124,32 @@ defmodule Sentry.Plug do
96124

97125
def remote_port({_, port}), do: port
98126

99-
defp handle_request_data(_conn, nil), do: %{}
100-
defp handle_request_data(conn, {module, fun}) do
127+
defp handle_data(_conn, nil), do: %{}
128+
defp handle_data(conn, {module, fun}) do
101129
apply(module, fun, [conn])
102130
end
103-
defp handle_request_data(conn, fun) when is_function(fun) do
131+
defp handle_data(conn, fun) when is_function(fun) do
104132
fun.(conn)
105133
end
106134

107135
## TODO also reject too big
108136

109-
defp scrub_headers(data) do
110-
Map.drop(data, ~w(authorization authentication))
137+
def default_header_scrubber(conn) do
138+
Enum.into(conn.req_headers, %{})
139+
|> Map.drop(@default_scrubbed_header_keys)
140+
end
141+
142+
def default_body_scrubber(conn) do
143+
conn.params
144+
|> Enum.map(fn({key, value}) ->
145+
value = cond do
146+
Enum.member?(@default_scrubbed_param_keys, key) -> @scrubbed_value
147+
Regex.match?(@credit_card_regex, value) -> @scrubbed_value
148+
true -> value
149+
end
150+
151+
{key, value}
152+
end)
153+
|> Enum.into(%{})
111154
end
112155
end

test/plug_test.exs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ defmodule Sentry.PlugTest do
5050
|> put_req_cookie("cookie_key", "cookie_value")
5151
|> put_req_header("accept-language", "en-US")
5252

53-
request_data = Sentry.Plug.build_request_interface_data(conn, [])
53+
request_data = Sentry.Plug.build_request_interface_data(conn, [header_scrubber: &Sentry.Plug.default_header_scrubber/1])
5454

5555
assert request_data[:url] =~ ~r/\/error_route$/
5656
assert request_data[:method] == "GET"
@@ -82,7 +82,8 @@ defmodule Sentry.PlugTest do
8282
|> Enum.into(%{})
8383
end
8484

85-
request_data = Sentry.Plug.build_request_interface_data(conn, scrubber: scrubber)
85+
options = [body_scrubber: scrubber, header_scrubber: &Sentry.Plug.default_header_scrubber/1]
86+
request_data = Sentry.Plug.build_request_interface_data(conn, options)
8687
assert request_data[:method] == "POST"
8788
assert request_data[:data] == %{"hello" => "world"}
8889
assert request_data[:headers] == %{"cookie" => "cookie_key=cookie_value", "accept-language" => "en-US", "content-type" => "multipart/mixed; charset: utf-8"}
@@ -96,4 +97,20 @@ defmodule Sentry.PlugTest do
9697
request_data = Sentry.Plug.build_request_interface_data(conn, [request_id_header: "x-request-id"])
9798
assert request_data[:env]["REQUEST_ID"] == "my_request_id"
9899
end
100+
101+
test "default data scrubbing" do
102+
conn = conn(:post, "/error_route", %{
103+
"secret" => "world",
104+
"password" => "test",
105+
"passwd" => "4242424242424242",
106+
"credit_card" => "4197 7215 7810 8280",
107+
"cc" => "4197-7215-7810-8280",
108+
"another_cc" => "4197721578108280"})
109+
110+
request_data = Sentry.Plug.build_request_interface_data(conn, body_scrubber: &Sentry.Plug.default_body_scrubber/1)
111+
assert request_data[:method] == "POST"
112+
assert request_data[:data] == %{"secret" => "*********", "password" => "*********",
113+
"passwd" => "*********", "credit_card" => "*********", "cc" => "*********",
114+
"another_cc" => "*********"}
115+
end
99116
end

0 commit comments

Comments
 (0)