Skip to content

Commit 1fc0c5e

Browse files
authored
Attempt to scrub all Plug.Conns in Sentry.PlugCapture (#619)
Closes #477.
1 parent 0e288a4 commit 1fc0c5e

File tree

2 files changed

+210
-112
lines changed

2 files changed

+210
-112
lines changed

lib/sentry/plug_capture.ex

Lines changed: 98 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
defmodule Sentry.PlugCapture do
22
@moduledoc """
3-
Provides basic functionality to handle and send errors occurring within
3+
Provides basic functionality to capture and send errors occurring within
44
Plug applications, including Phoenix.
55
6-
It is intended for usage with `Sentry.PlugContext`.
6+
It is intended for usage with `Sentry.PlugContext`, which adds relevant request
7+
metadata to the Sentry context before errors are captured.
78
89
## Usage
910
11+
### With Phoenix
12+
1013
In a Phoenix application, it is important to use this module **before**
1114
the Phoenix endpoint itself. It should be added to your `endpoint.ex` file:
1215
@@ -17,7 +20,9 @@ defmodule Sentry.PlugCapture do
1720
# ...
1821
end
1922
20-
In a Plug application, it can be added *below* your router:
23+
### With Plug
24+
25+
In a Plug application, you can add this module *below* your router:
2126
2227
defmodule MyApp.PlugRouter do
2328
use Plug.Router
@@ -32,30 +37,76 @@ defmodule Sentry.PlugCapture do
3237
> and adds capturing errors and reporting to Sentry. You can still re-override
3338
> that callback after `use Sentry.PlugCapture` if you need to.
3439
40+
## Scrubbing Sensitive Data
41+
42+
> #### Since v9.1.0 {: .neutral}
43+
>
44+
> Scrubbing sensitive data in `Sentry.PlugCapture` is available since v9.1.0
45+
> of this library.
46+
47+
Like `Sentry.PlugContext`, this module also supports scrubbing sensitive data
48+
out of errors. However, this module has to do some *guessing* to figure
49+
out if there are `Plug.Conn` structs to scrub. Right now, the strategy we
50+
use follows these steps:
51+
52+
1. if the error is `Phoenix.ActionClauseError`, we scrub the `Plug.Conn` structs
53+
from the `args` field of that exception
54+
55+
Otherwise, we don't perform any scrubbing. To configure scrubbing, you can use the
56+
`:scrubbing` option (see below).
57+
58+
## Options
59+
60+
* `:scrubber` (since v9.1.0) - a term of type `{module, function, args}` that
61+
will be invoked to scrub sensitive data from `Plug.Conn` structs. The
62+
`Plug.Conn` struct is prepended to `args` before invoking the function,
63+
so that the final function will be called as `apply(module, function, [conn | args])`.
64+
The function must return a `Plug.Conn` struct. By default, the built-in
65+
scrubber does this:
66+
67+
* scrubs *all* cookies
68+
* scrubs sensitive headers just like `Sentry.PlugContext.default_header_scrubber/1`
69+
* scrubs sensitive body params just like `Sentry.PlugContext.default_body_scrubber/1`
70+
3571
"""
3672

37-
defmacro __using__(_opts) do
73+
defmacro __using__(opts) do
3874
quote do
75+
opts = unquote(Macro.escape(opts))
76+
default_scrubber = {unquote(__MODULE__), :default_scrubber, []}
77+
78+
scrubber =
79+
case Keyword.get(opts, :scrubber, default_scrubber) do
80+
{mod, fun, args} = scrubber when is_atom(mod) and is_atom(fun) and is_list(args) ->
81+
scrubber
82+
83+
other ->
84+
raise ArgumentError,
85+
"expected :scrubber to be a {module, function, args} tuple, got: #{inspect(other)}"
86+
end
87+
88+
@__sentry_scrubber scrubber
89+
3990
@before_compile Sentry.PlugCapture
4091
end
4192
end
4293

43-
defmacro __before_compile__(_) do
94+
defmacro __before_compile__(_env) do
4495
quote do
4596
defoverridable call: 2
4697

4798
def call(conn, opts) do
4899
try do
49100
super(conn, opts)
50101
rescue
51-
e in Plug.Conn.WrapperError ->
52-
exception = Exception.normalize(:error, e.reason, e.stack)
53-
_ = Sentry.capture_exception(exception, stacktrace: e.stack, event_source: :plug)
54-
Plug.Conn.WrapperError.reraise(e)
55-
56-
e ->
57-
_ = Sentry.capture_exception(e, stacktrace: __STACKTRACE__, event_source: :plug)
58-
:erlang.raise(:error, e, __STACKTRACE__)
102+
err in Plug.Conn.WrapperError ->
103+
exception = Exception.normalize(:error, err.reason, err.stack)
104+
Sentry.PlugCapture.__capture_exception__(exception, err.stack, @__sentry_scrubber)
105+
Plug.Conn.WrapperError.reraise(err)
106+
107+
exc ->
108+
Sentry.PlugCapture.__capture_exception__(exc, __STACKTRACE__, @__sentry_scrubber)
109+
:erlang.raise(:error, exc, __STACKTRACE__)
59110
catch
60111
kind, reason ->
61112
message = "Uncaught #{kind} - #{inspect(reason)}"
@@ -66,4 +117,38 @@ defmodule Sentry.PlugCapture do
66117
end
67118
end
68119
end
120+
121+
@doc false
122+
def __capture_exception__(exception, stacktrace, scrubber) do
123+
# We can't pattern match here, because we're not guaranteed to have
124+
# Phoenix available.
125+
exception =
126+
if is_struct(exception, Phoenix.ActionClauseError) do
127+
update_in(exception, [Access.key!(:args), Access.all()], fn
128+
conn when is_struct(conn, Plug.Conn) -> apply_scrubber(conn, scrubber)
129+
other -> other
130+
end)
131+
else
132+
exception
133+
end
134+
135+
Sentry.capture_exception(exception, stacktrace: stacktrace, event_source: :plug)
136+
end
137+
138+
@doc false
139+
def default_scrubber(conn) do
140+
%{
141+
conn
142+
| cookies: %{},
143+
req_headers: Sentry.PlugContext.default_header_scrubber(conn),
144+
params: Sentry.PlugContext.default_body_scrubber(conn)
145+
}
146+
end
147+
148+
defp apply_scrubber(conn, {mod, fun, args} = _scrubber) do
149+
case apply(mod, fun, [conn | args]) do
150+
conn when is_struct(conn, Plug.Conn) -> conn
151+
other -> raise ":scrubber function must return a Plug.Conn struct, got: #{inspect(other)}"
152+
end
153+
end
69154
end

0 commit comments

Comments
 (0)