Add URI config validator/type

Often times plugins (like the Elasticsearch output) can naturally use URIs for their configuration.
Unfortunately using the :string type here means that the password portion of the URI can easily be leaked.

This wraps the URI class in a new LogStash::Util::SafeURI class that proxies all regular URI methods but masks
the password when `#to_s` and `#inspect` are invoked.

Fixes #5439
This commit is contained in:
Andrew Cholakian 2016-06-02 18:26:27 -05:00
parent 6ae54801da
commit 53ed1defd0
4 changed files with 140 additions and 0 deletions

View file

@ -203,6 +203,21 @@ Example:
my_password => "password" my_password => "password"
---------------------------------- ----------------------------------
[[uri]]
[float]
==== URI
A URI can be anything from a full URL like 'http://elastic.co/' to a simple identifier
like 'foobar'. If the URI contains a password such as 'http://user:pass@example.net' the password
portion of the URI will not be logged or printed.
Example:
[source,js]
----------------------------------
my_uri => "http://foo:bar@example.net"
----------------------------------
[[path]] [[path]]
[float] [float]
==== Path ==== Path

View file

@ -4,6 +4,7 @@ require "logstash/config/registry"
require "logstash/plugins/registry" require "logstash/plugins/registry"
require "logstash/logging" require "logstash/logging"
require "logstash/util/password" require "logstash/util/password"
require "logstash/util/safe_uri"
require "logstash/version" require "logstash/version"
require "logstash/environment" require "logstash/environment"
require "logstash/util/plugin_version" require "logstash/util/plugin_version"
@ -513,6 +514,12 @@ module LogStash::Config::Mixin
end end
result = value.first.is_a?(::LogStash::Util::Password) ? value.first : ::LogStash::Util::Password.new(value.first) result = value.first.is_a?(::LogStash::Util::Password) ? value.first : ::LogStash::Util::Password.new(value.first)
when :uri
if value.size > 1
return false, "Expected uri (one value), got #{value.size} values?"
end
result = value.first.is_a?(::LogStash::Util::SafeURI) ? value.first : ::LogStash::Util::SafeURI.new(value.first)
when :path when :path
if value.size > 1 # Only 1 value wanted if value.size > 1 # Only 1 value wanted
return false, "Expected path (one value), got #{value.size} values?" return false, "Expected path (one value), got #{value.size} values?"
@ -551,6 +558,10 @@ module LogStash::Config::Mixin
if @config[key][:validate] == :password && !value.is_a?(::LogStash::Util::Password) if @config[key][:validate] == :password && !value.is_a?(::LogStash::Util::Password)
params[key] = ::LogStash::Util::Password.new(value) params[key] = ::LogStash::Util::Password.new(value)
end end
if @config[key][:validate] == :uri && !value.is_a?(::LogStash::Util::SafeURI)
params[key] = ::LogStash::Util::SafeURI.new(value)
end
end end
end end

View file

@ -0,0 +1,44 @@
# encoding: utf-8
require "logstash/namespace"
require "logstash/util"
# This class exists to quietly wrap a password string so that, when printed or
# logged, you don't accidentally print the password itself.
class LogStash::Util::SafeURI
PASS_PLACEHOLDER = "xxxxxx".freeze
extend Forwardable
def_delegators :@uri, :coerce, :query=, :route_from, :port=, :default_port, :select, :normalize!, :absolute?, :registry=, :path, :password, :hostname, :merge, :normalize, :host, :component_ary, :userinfo=, :query, :set_opaque, :+, :merge!, :-, :password=, :parser, :port, :set_host, :set_path, :opaque=, :scheme, :fragment=, :set_query, :set_fragment, :userinfo, :hostname=, :set_port, :path=, :registry, :opaque, :route_to, :set_password, :hierarchical?, :set_user, :set_registry, :set_userinfo, :fragment, :component, :user=, :set_scheme, :absolute, :host=, :relative?, :scheme=, :user
attr_reader :uri
public
def initialize(arg)
@uri = case arg
when String
URI.parse(arg)
when URI
arg
else
raise ArgumentError, "Expected a string or URI, got a #{arg.class} creating a URL"
end
end
def to_s
sanitized.to_s
end
def inspect
sanitized.to_s
end
def sanitized
return uri unless uri.password # nothing to sanitize here!
safe = uri.clone
safe.password = PASS_PLACEHOLDER
safe
end
end

View file

@ -102,6 +102,76 @@ describe LogStash::Config::Mixin do
end end
end end
context "when validating :uri" do
let(:klass) do
Class.new(LogStash::Filters::Base) do
config_name "fakeuri"
config :uri, :validate => :uri
end
end
shared_examples("safe URI") do
subject { klass.new("uri" => uri_str) }
it "should be a SafeURI object" do
expect(subject.uri).to(be_a(LogStash::Util::SafeURI))
end
it "should make password values hidden with #to_s" do
expect(subject.uri.to_s).to eql(uri_hidden)
end
it "should make password values hidden with #inspect" do
expect(subject.uri.inspect).to eql(uri_hidden)
end
it "should correctly copy URI types" do
clone = subject.class.new(subject.params)
expect(clone.uri.to_s).to eql(uri_hidden)
end
it "should make the real URI object availale under #uri" do
expect(subject.uri.uri).to be_a(::URI)
end
it "should obfuscate original_params" do
expect(subject.original_params['uri']).to(be_a(LogStash::Util::SafeURI))
end
context "attributes" do
[:scheme, :user, :password, :hostname, :path].each do |attr|
it "should make #{attr} available" do
expect(subject.uri.send(attr)).to eql(self.send(attr))
end
end
end
end
context "with a username / password" do
let(:scheme) { "myscheme" }
let(:user) { "myuser" }
let(:password) { "fancypants" }
let(:hostname) { "myhostname" }
let(:path) { "/my/path" }
let(:uri_str) { "#{scheme}://#{user}:#{password}@#{hostname}#{path}" }
let(:uri_hidden) { "#{scheme}://#{user}:#{LogStash::Util::SafeURI::PASS_PLACEHOLDER}@#{hostname}#{path}" }
include_examples("safe URI")
end
context "without a username / password" do
let(:scheme) { "myscheme" }
let(:user) { nil }
let(:password) { nil }
let(:hostname) { "myhostname" }
let(:path) { "/my/path" }
let(:uri_str) { "#{scheme}://#{hostname}#{path}" }
let(:uri_hidden) { "#{scheme}://#{hostname}#{path}" }
include_examples("safe URI")
end
end
describe "obsolete settings" do describe "obsolete settings" do
let(:plugin_class) do let(:plugin_class) do
Class.new(LogStash::Inputs::Base) do Class.new(LogStash::Inputs::Base) do