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

Add cookies as an optional property in the request handler #2167

Conversation

ThatTobMate
Copy link
Contributor

@ThatTobMate ThatTobMate commented Jul 16, 2019

We are using the requestHandler in our express application but we don't want cookies to be added to all of our errors as some cookies contain personal data.
We have used the settings in the Sentry UI to set cookies as a sensitive field and we also have a beforeSend block that deletes cookies from the error event.
Neither of these have prevented the cookies being added to our errors.

This PR allows users to configure if they want cookies to be added to the request data. It feels like a reasonable configuration option to provide given the likelihood of cookies containing some personal data.

  • If you've added code that should be tested, please add tests.
  • Ensure your code lints and the test suite passes (yarn lint) & (yarn test).

@kamilogorek
Copy link
Contributor

Thanks for the PR! As extractRequestData extracts more than just cookies, I think it'd be a good idea to tackle this issue, in the same manner, we do with user, by providing an array of keys to extract.
If we add cookies to the top-level options, and someone comes by and asks for the same but for body, or for query, this list will grow further and further.
We can modify extractRequestData itself to accept the second argument of options and mimick behavior of extractUserData

/** Default user keys that'll be used to extract data from the request */
const DEFAULT_USER_KEYS = ['id', 'username', 'email'];
/** JSDoc */
function extractUserData(req: { [key: string]: any }, keys: boolean | string[]): { [key: string]: string } {
const user: { [key: string]: string } = {};
const attributes = Array.isArray(keys) ? keys : DEFAULT_USER_KEYS;
attributes.forEach(key => {
if ({}.hasOwnProperty.call(req.user, key)) {
user[key] = (req.user as { [key: string]: string })[key];
}
});
// client ip:
// node: req.connection.remoteAddress
// express, koa: req.ip
const ip =
req.ip ||
(req.connection &&
(req.connection as {
remoteAddress?: string;
}).remoteAddress);
if (ip) {
user.ip_address = ip as string;
}
return user;
}

 - Revert cookie configuration option.
 - Add optional array to request argument
 - Filter any keys from the request interface that don't match the array
@@ -112,6 +112,17 @@ function extractRequestData(req: { [key: string]: any }): { [key: string]: strin
url: absoluteUrl,
};

const attributes = Array.isArray(keys) ? keys : [];

if (attributes.length) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using slightly different logic here than the extractUserData function, as you are already running some custom logic to set/manipulate keys on the request interface.
In this PR we build the request interface as before then remove any of the properties not specified in the optional array (if it exists).

This behaviour is slightly different to the extractUserData fn as it allows users to request any of the properties from the request interface rather than any properties of the req argument itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If that's a bit too ambiguous we could do something like below which would allow users to extract any property on the req object:

  1. Set the requested properties
  2. Run the rest of the current logic to overwrite the values
  3. Merge the requested properties with the default values
  4. Delete any keys that don't match the requested properties from the array.
function extractRequestData(...) {
  // 1.
  const request: { [key: string]: string } = {};
  const attributes = Array.isArray(keys) ? keys : [];

  attributes.forEach(key => {
    if ({}.hasOwnProperty.call(req, key)) {
      request[key] = (req as { [key: string]: string })[key];
    }
  });

  ----
  // 2.
  // set values
  const headers = ...
  const method = ...
  ----

  // 3.
  // request interface
  const requestInterface: {
    [key: string]: any;
  } = {
    ...request
    cookies,
    data,
    headers,
    method,
    query_string: query,
    url: absoluteUrl,
  };

 ----
 // 4.
 // Delete any of the default keys not specified in the array
  if (attributes.length) {
    Object.keys(requestInterface).forEach(key => {
      /** Remove any of the unspecified keys in the options from the request interface */
      if (!attributes.includes(key)) {
        delete requestInterface[key];
      }
    });
  }

 return requestInterface
}

},
};

describe('parseRequest.user properties', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thought i'd add a bit of test coverage for some of this logic for both the user and request properties

@kamilogorek
Copy link
Contributor

Everything looks great (and thanks for the tests) except one thing, which is data extraction. Calling JSON.stringify(normalize(data)) may be costly, and if we go with "remove after assigned" solution, then we'll stringify data for each call for no reason.

We can make use of your second idea, with a slight twist by using switch statement to make it easier to read and make a fall-through to the regular assignment. Eg.:

function extractRequestData(req, keys) {
  const request = {};

  (Array.isArray(keys) ? keys : DEFAULT_KEYS).forEach(key => {
    switch (key) {
      case "headers":
        request.headers = req.headers || req.header || {};
        break;
      case "protocol":
        request.protocol =
          req.protocol === "https" || req.secure || (req.socket || {}).encrypted
            ? "https"
            : "http";
        break;
      case "host":
        request.host = req.hostname || req.host || headers.host || "<no host>";
        break;
      default:
        if ({}.hasOwnProperty.call(req, key)) {
          request[key] = req[key];
        }
    }
  });

  return request;
}

Copy link
Contributor

@kamilogorek kamilogorek left a comment

Choose a reason for hiding this comment

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

See comment above.

@kamilogorek
Copy link
Contributor

kamilogorek commented Aug 6, 2019

Reworked it just a bit and merged manually to make it into 5.6.0 release. Your work and tests has been preserved :) Thanks for the PR and all the changes!

https://github.com/getsentry/sentry-javascript/commits/master

@kamilogorek kamilogorek closed this Aug 6, 2019
@ThatTobMate
Copy link
Contributor Author

Reworked it just a bit and merged manually to make it into 5.6.0 release. Your work and tests has been preserved :) Thanks for the PR and all the changes!

https://github.com/getsentry/sentry-javascript/commits/master

Legend, thanks for that 👍

@ThatTobMate ThatTobMate deleted the tobias/make-cookies-optional-in-req branch August 7, 2019 12:26
@kamilogorek
Copy link
Contributor

Corresponding docs PR: getsentry/sentry-docs#1145

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants