Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attempt to scrub all Plug.Conns in Sentry.PlugCapture #619

Merged
merged 2 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
111 changes: 98 additions & 13 deletions lib/sentry/plug_capture.ex
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
defmodule Sentry.PlugCapture do
@moduledoc """
Provides basic functionality to handle and send errors occurring within
Provides basic functionality to capture and send errors occurring within
Plug applications, including Phoenix.

It is intended for usage with `Sentry.PlugContext`.
It is intended for usage with `Sentry.PlugContext`, which adds relevant request
metadata to the Sentry context before errors are captured.

## Usage

### With Phoenix

In a Phoenix application, it is important to use this module **before**
the Phoenix endpoint itself. It should be added to your `endpoint.ex` file:

Expand All @@ -17,7 +20,9 @@ defmodule Sentry.PlugCapture do
# ...
end

In a Plug application, it can be added *below* your router:
### With Plug

In a Plug application, you can add this module *below* your router:

defmodule MyApp.PlugRouter do
use Plug.Router
Expand All @@ -32,30 +37,76 @@ defmodule Sentry.PlugCapture do
> and adds capturing errors and reporting to Sentry. You can still re-override
> that callback after `use Sentry.PlugCapture` if you need to.

## Scrubbing Sensitive Data

> #### Since v9.1.0 {: .neutral}
>
> Scrubbing sensitive data in `Sentry.PlugCapture` is available since v9.1.0
> of this library.

Like `Sentry.PlugContext`, this module also supports scrubbing sensitive data
out of errors. However, this module has to do some *guessing* to figure
out if there are `Plug.Conn` structs to scrub. Right now, the strategy we
use follows these steps:

1. if the error is `Phoenix.ActionClauseError`, we scrub the `Plug.Conn` structs
from the `args` field of that exception

Otherwise, we don't perform any scrubbing. To configure scrubbing, you can use the
`:scrubbing` option (see below).

## Options

* `:scrubber` (since v9.1.0) - a term of type `{module, function, args}` that
will be invoked to scrub sensitive data from `Plug.Conn` structs. The
`Plug.Conn` struct is prepended to `args` before invoking the function,
so that the final function will be called as `apply(module, function, [conn | args])`.
The function must return a `Plug.Conn` struct. By default, the built-in
scrubber does this:

* scrubs *all* cookies
* scrubs sensitive headers just like `Sentry.PlugContext.default_header_scrubber/1`
* scrubs sensitive body params just like `Sentry.PlugContext.default_body_scrubber/1`

"""

defmacro __using__(_opts) do
defmacro __using__(opts) do
quote do
opts = unquote(Macro.escape(opts))
default_scrubber = {unquote(__MODULE__), :default_scrubber, []}

scrubber =
case Keyword.get(opts, :scrubber, default_scrubber) do
{mod, fun, args} = scrubber when is_atom(mod) and is_atom(fun) and is_list(args) ->
scrubber

other ->
raise ArgumentError,
"expected :scrubber to be a {module, function, args} tuple, got: #{inspect(other)}"
end

@__sentry_scrubber scrubber

@before_compile Sentry.PlugCapture
end
end

defmacro __before_compile__(_) do
defmacro __before_compile__(_env) do
quote do
defoverridable call: 2

def call(conn, opts) do
try do
super(conn, opts)
rescue
e in Plug.Conn.WrapperError ->
exception = Exception.normalize(:error, e.reason, e.stack)
_ = Sentry.capture_exception(exception, stacktrace: e.stack, event_source: :plug)
Plug.Conn.WrapperError.reraise(e)

e ->
_ = Sentry.capture_exception(e, stacktrace: __STACKTRACE__, event_source: :plug)
:erlang.raise(:error, e, __STACKTRACE__)
err in Plug.Conn.WrapperError ->
exception = Exception.normalize(:error, err.reason, err.stack)
Sentry.PlugCapture.__capture_exception__(exception, err.stack, @__sentry_scrubber)
Plug.Conn.WrapperError.reraise(err)

exc ->
Sentry.PlugCapture.__capture_exception__(exc, __STACKTRACE__, @__sentry_scrubber)
:erlang.raise(:error, exc, __STACKTRACE__)
catch
kind, reason ->
message = "Uncaught #{kind} - #{inspect(reason)}"
Expand All @@ -66,4 +117,38 @@ defmodule Sentry.PlugCapture do
end
end
end

@doc false
def __capture_exception__(exception, stacktrace, scrubber) do
# We can't pattern match here, because we're not guaranteed to have
# Phoenix available.
exception =
if is_struct(exception, Phoenix.ActionClauseError) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't doing this just for ActionClauseError too limited?
like it solves the issue but similar leakage could happen in other exceptions too right?

update_in(exception, [Access.key!(:args), Access.all()], fn
conn when is_struct(conn, Plug.Conn) -> apply_scrubber(conn, scrubber)
other -> other
end)
else
exception
end

Sentry.capture_exception(exception, stacktrace: stacktrace, event_source: :plug)
end

@doc false
def default_scrubber(conn) do
%{
conn
| cookies: %{},
req_headers: Sentry.PlugContext.default_header_scrubber(conn),
params: Sentry.PlugContext.default_body_scrubber(conn)
}
end

defp apply_scrubber(conn, {mod, fun, args} = _scrubber) do
case apply(mod, fun, [conn | args]) do
conn when is_struct(conn, Plug.Conn) -> conn
other -> raise ":scrubber function must return a Plug.Conn struct, got: #{inspect(other)}"
end
end
end