RUBY-3835 redact credentials in URI rendering and InvalidURI errors#3039
RUBY-3835 redact credentials in URI rendering and InvalidURI errors#3039comandeo-mongo wants to merge 2 commits intomongodb:masterfrom
Conversation
URI#to_s, URI#inspect, and Mongo::Error::InvalidURI all rendered the cleartext password from the original connection string. The output of each could end up in application logs, error reporters, or backtraces, exposing credentials beyond the trust boundary. Replace the userinfo with a fixed placeholder when reconstructing the URI for display, and override inspect so it does not dump @string or @password. Redact the URI passed to InvalidURI before interpolating it into the message.
There was a problem hiding this comment.
Pull request overview
This PR addresses credential leakage risks by ensuring MongoDB connection strings are rendered with credentials redacted when converted to strings, inspected, or embedded into InvalidURI error messages.
Changes:
- Added
Mongo::URI.redact(string)and updated URI string rendering (#to_s,#inspect) to use a<credentials>placeholder. - Updated
Mongo::Error::InvalidURIto redact URIs before interpolating into the error message. - Updated/added specs so roundtrip expectations and error-message assertions validate redacted output.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
lib/mongo/uri.rb |
Introduces redaction helper/constants; updates #to_s and #inspect to avoid leaking credentials. |
lib/mongo/error/invalid_uri.rb |
Redacts the provided URI before interpolating it into the exception message. |
spec/mongo/uri_spec.rb |
Adds unit tests for .redact, #to_s redaction, and #inspect redaction; updates shared examples. |
spec/mongo/uri/srv_protocol_spec.rb |
Updates expectations to compare against redacted to_s output for SRV URIs. |
spec/mongo/error/invalid_uri_spec.rb |
Adds coverage ensuring InvalidURI messages redact credentials (including nil handling). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Anchors at the start and stops at the first '/', '?', or '#' so it | ||
| # cannot accidentally redact something past the authority component. | ||
| USERINFO_REDACTION_REGEX = %r{\A(mongodb(?:\+srv)?://)[^/?#@]*@}.freeze |
There was a problem hiding this comment.
These are fair points I think. The necessity to securely handle malformed URI's means this code needs to be especially liberal in how it applies constraints. It's better to hide too much than too little, I think. If we match to the last @ instead of the first, that should address any leakage caused by the password itself containing an @ symbol. And making the regex case-insensitive is probably a good idea, too.
| describe '.redact' do | ||
| it 'returns nil unchanged' do | ||
| expect(described_class.redact(nil)).to be_nil | ||
| end | ||
|
|
||
| it 'leaves a uri without userinfo unchanged' do | ||
| expect(described_class.redact('mongodb://localhost:27017')) | ||
| .to eq('mongodb://localhost:27017') | ||
| end | ||
|
|
||
| it 'replaces user and password with the placeholder' do | ||
| expect(described_class.redact('mongodb://alice:s3cret@host:27017/admin')) | ||
| .to eq('mongodb://<credentials>@host:27017/admin') | ||
| end | ||
|
|
||
| it 'redacts userinfo for mongodb+srv:// URIs' do | ||
| expect(described_class.redact('mongodb+srv://alice:s3cret@host')) | ||
| .to eq('mongodb+srv://<credentials>@host') | ||
| end | ||
|
|
||
| it 'redacts user-only userinfo' do | ||
| expect(described_class.redact('mongodb://alice@host')) | ||
| .to eq('mongodb://<credentials>@host') | ||
| end |
jamis
left a comment
There was a problem hiding this comment.
I agree with Copilot on these issues it has raised; probably best to apply redactions too liberally than not liberally enough.
| # Anchors at the start and stops at the first '/', '?', or '#' so it | ||
| # cannot accidentally redact something past the authority component. | ||
| USERINFO_REDACTION_REGEX = %r{\A(mongodb(?:\+srv)?://)[^/?#@]*@}.freeze |
There was a problem hiding this comment.
These are fair points I think. The necessity to securely handle malformed URI's means this code needs to be especially liberal in how it applies constraints. It's better to hide too much than too little, I think. If we match to the last @ instead of the first, that should address any leakage caused by the password itself containing an @ symbol. And making the regex case-insensitive is probably a good idea, too.
| describe '.redact' do | ||
| it 'returns nil unchanged' do | ||
| expect(described_class.redact(nil)).to be_nil | ||
| end | ||
|
|
||
| it 'leaves a uri without userinfo unchanged' do | ||
| expect(described_class.redact('mongodb://localhost:27017')) | ||
| .to eq('mongodb://localhost:27017') | ||
| end | ||
|
|
||
| it 'replaces user and password with the placeholder' do | ||
| expect(described_class.redact('mongodb://alice:s3cret@host:27017/admin')) | ||
| .to eq('mongodb://<credentials>@host:27017/admin') | ||
| end | ||
|
|
||
| it 'redacts userinfo for mongodb+srv:// URIs' do | ||
| expect(described_class.redact('mongodb+srv://alice:s3cret@host')) | ||
| .to eq('mongodb+srv://<credentials>@host') | ||
| end | ||
|
|
||
| it 'redacts user-only userinfo' do | ||
| expect(described_class.redact('mongodb://alice@host')) | ||
| .to eq('mongodb://<credentials>@host') | ||
| end |
Tightens USERINFO_REDACTION_REGEX to handle two malformed-input cases that the previous pattern leaked credentials on: - Passwords containing an unescaped '@' (e.g. mongodb://alice:p@ss@host) now redact through the last '@' in the authority component instead of stopping at the first one. The character class drops '@' and relies on greedy backtracking bounded by '/', '?', '#' to find the right '@'. - Schemes in mixed case (e.g. MongoDB://...) are now matched. The driver parser rejects these, so they always flow through the redactor on the way to InvalidURI; missing them guarantees a leak in the error. Adds specs for both edge cases.
What
Cleartext passwords passed in the MongoDB connection string were exposed in three places:
Mongo::URI#to_s—reconstruct_uriinterpolated@passworddirectly.Mongo::URI#inspect— Ruby's default dumped@stringand@passwordinstance variables.Mongo::Error::InvalidURI—"Bad URI: #{uri}\n"interpolated the raw input.Anywhere the result hit a logger, error reporter (Sentry/Bugsnag/Rollbar), or a backtrace surfaced in a UI, the password left the trust boundary.
How
Mongo::URI.redact(string)class method returns the URI with userinfo replaced by a<credentials>placeholder. It works on raw strings (regex-based), so it is safe to call on malformed input — which is exactly whenInvalidURIis raised.URI#to_sandURI#inspectnow use the placeholder.inspectno longer dumps instance variables.Mongo::Error::InvalidURIredacts the URI argument before interpolation.roundtrips stringshared examples now compare against the redacted form, so non-credential test fixtures are unaffected. Three test cases that hardcoded the credential-bearing output were updated.Test plan
bundle exec rubocop lib/mongo/uri.rb lib/mongo/error/invalid_uri.rb spec/mongo/uri_spec.rb spec/mongo/uri/srv_protocol_spec.rb spec/mongo/error/invalid_uri_spec.rb— clean.MONGODB_URI=... bundle exec rspec spec/mongo/uri_spec.rb spec/mongo/error/invalid_uri_spec.rb spec/mongo/uri_option_parsing_spec.rb spec/spec_tests/uri_options_spec.rb— 722 examples, 0 failures.MONGODB_URI=... bundle exec rspec spec/mongo/uri/srv_protocol_spec.rb— pending tests skip (no DNS), no failures.URI.redact,URI#to_sredaction,URI#inspectredaction,InvalidURIredaction formongodb://andmongodb+srv://,nilURI handling.Jira
RUBY-3835