-
Notifications
You must be signed in to change notification settings - Fork 26
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
Redact potentially sensitive information from error objects #76
Conversation
src/security.ts
Outdated
*/ | ||
export function redactObject (obj: Record<string, any>, additionalKeys: string[] = []): Record<string, any> { | ||
const toRedact = [...secretKeys, ...additionalKeys].map(key => key.toLowerCase()) | ||
return traverse(obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
traverse
loops recursively through every key/value in any deeply-nested object, and can easily account for circular references (see line 47). I wanted both of these things so that this function could be generic enough to be used elsewhere later.
It was proposed that we could use fast-redact
to implement this redaction feature. However, it uses wildcards to match a key string at multiple levels. I benchmarked it against my solution and, when using it to match keys at 5+ levels of nesting—DiagnosticResult
includes HTTP headers nested 5 levels deep—it was substantially slower than using traverse
, and did not account for circular references.
This validation would have been ideal, but it would break this lib's contract with elasticsearch-js. Since we want to ensure the transport's major version will also work with the same major version of Elasticsearch, bumping this lib to 9.0.0 would have created confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should provide some documentation on this feature and how a user can customize the list of keyword to be filtered. Moreover, do we provide a way to switch off this features? I'm assuming we'll remove the sensitive information by default but if someone wants to have it we should provide (e.g. for debugging).
|
||
import traverse from 'traverse' | ||
|
||
const secretKeys = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can a user customize this list?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm always a bit on the fence about adding new options, users may overlook the documentation and adds more code to maintain in general. If the goal here is only to hide the sensitive options, how about using a getter?
For example:
class Error {
constructor (opts) {
this.foo = opts.foo
Object.defineProperty(this, 'bar', {
get: () => opts.bar
})
}
}
const error = new Error({ foo: 1, bar: 2 })
console.log(error)
console.log(error.bar)
throw error
This will log the following:
Error { foo: 1 }
2
/Users/delvedor/Development/elastic/sdh-stats/error.js:13
throw error
^
Error { foo: 1 }
Node.js v18.12.1
In the Transport's case we could do:
export class ConnectionError extends ElasticsearchClientError {
meta?: DiagnosticResult
constructor (message: string, meta?: DiagnosticResult) {
super(message)
Error.captureStackTrace(this, ConnectionError)
this.name = 'ConnectionError'
this.message = message ?? 'Connection Error'
Object.defineProperty(this, 'meta', {
get: () => meta
})
}
}
The same in other error classes. It may not be conventional, but it does the trick without introducing new options.
I agree that adding new options increases maintenance cost and should be avoided when possible, but it didn't seem reasonable in this case. The goal of the options is to allow multiple redaction "strengths," and to be able to disable it when the old behavior is needed for e.g. local debugging. When discussing with @rudolf, @pgayvallet and @ezimuel, adding these options—with secure but non-breaking-change defaults—seemed like the most flexible approach: current users get better protection by default, users who want more protection can add it, and it can be disabled when necessary. @delvedor Did you have an idea of how to achieve the same goals with getters? It wasn't clear by your example. |
@ezimuel The three new options in the PR description are how all of these things can be customized. The optional list of redaction keys ( As for docs, when I upgrade elasticsearch-js to use this new transport version, I'll add documentation there, as well as some notes in the changelog. |
Just to clarify, these existing protections only work on I think both layers are usefulh bb |
@rudolf I assume you closed on accident 😆 |
I chatted with @delvedor earlier today and we talked through his suggestion in detail. Rather than adding a Rather than replacing values with
See commit b486c79 for this change.
Planning to merge this tomorrow, Dec 8. |
I'm not sure how I feel about this... I 100% agree that BUT *rant incoming 😉 * I feel like Elasticsearch-js has no business throwing around secrets all over the place. "I asked you to call Elasticsearch for me and tell me what went wrong, the authorization headers you used to make the request are irrelevant!" If I e.g. compare mongodb's nodejs client errors just contain the code, message, labels and a connection id string https://github.com/mongodb/node-mongodb-native/blob/main/src/error.ts I know there's some backwards compatibility challenges, but if I could be completely selfish I just want an error that contains the bare minimum for me to understand what went wrong. Anything more I would rather use the diagnostics event emitter to debug. Things to include would be (probably missing something):
But definitely I don't want the request/response headers or the connection they're not useful and they're dangerous so why add it to the error object in the first place. Removing these completely makes me feel much better than trying to use a scalpel to find just the things we know are secrets. |
Thanks for the feedback, @rudolf. I agree full removal is ideal. A diagnostic looking more like your proposal is definitely a consideration we can take for the next major version. But since the I'm leaning toward reverting the last commit, and changing the redaction options to something like: interface RedactionOptions {
type: 'basic' | 'advanced' | 'off'
additionalKeys?: string[]
}
By nesting the three options into their own interface, the top-level option would just be |
Another set of options for
|
I settled for Thanks everyone for the feedback. This solution feels a lot more refined. 🙏 |
Otherwise it breaks compatibility with the client
I tested this branch against all minor release branches of elasticsearch-js back to 8.6. Glad I did. Quick fix in 5b1324d, and now all tests on those branches are passing. Merging soon. |
Not going to publish to npm until Monday, just to be safe. |
Includes support for [new redaction features](elastic/elastic-transport-js#76).
Exposes new
Transport
options to improve the JS client's security position on potentially sensitive data.Right now, many of the
Error
classes defined in this library include aDiagnosticResult
object full of request and connection metadata that is useful for debugging. This can include HTTP headers and URLs. Common sources of sensitive data are removed while serializing to JSON or logging to the console, so common use cases are already covered. However, since that data is still stored on these objects in memory, sensitive data may not be scrubbed when custom serialization methods are used.To solve for these cases,
Error
objects now fully redact and/or remove potentially sensitive data at instantiation time rather than at serialization time, so they do not exist in memory at all and cannot be exposed by any means.These redaction methods are all exposed as options on the
Transport
class:redactConnection
- Completely removes theConnection
object from theDiagnostictResult
(false
by default)redactDiagnostics
- Recursively traverses theDiagnosticResult
object, redacting any values whose keys match—case-insensitive—a pre-set list of sensitive key names:authorization
,password
,apiKey
, andx-elastic-app-auth
(true
by default)additionalRedactionKeys
- A list of additional keys that can be redacted whenredactDiagnostics
istrue
; note that it is not possible to remove the pre-set keys listed above, only add new ones (defaults to[]
)See elastic/elasticsearch-js#2026.