Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion lib/fluent/plugin/output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class Output < Base
CHUNK_KEY_PLACEHOLDER_PATTERN = /\$\{([-_.@$a-zA-Z0-9]+)\}/
CHUNK_TAG_PLACEHOLDER_PATTERN = /\$\{(tag(?:\[-?\d+\])?)\}/
CHUNK_ID_PLACEHOLDER_PATTERN = /\$\{chunk_id\}/
INVALID_PATH_COMPONENT_PATTERN = %r{\.\.[/\\]|^[/\\]}
PARENT_DIRECTORY_PATTERN = %r{\.\.[/\\]}

CHUNKING_FIELD_WARN_NUM = 4

Expand Down Expand Up @@ -825,9 +827,17 @@ def extract_placeholders(str, chunk)
# ${tag}, ${tag[0]}, ${tag[1]}, ... , ${tag[-2]}, ${tag[-1]}
if @chunk_key_tag
if str.include?('${tag}')
if metadata.tag.match?(INVALID_PATH_COMPONENT_PATTERN)
raise Fluent::UnrecoverableError, "Invalid path component detected in tag: #{metadata.tag}"
end

rvalue = rvalue.gsub('${tag}', metadata.tag)
end
if CHUNK_TAG_PLACEHOLDER_PATTERN.match?(str)
if metadata.tag.match?(INVALID_PATH_COMPONENT_PATTERN)
raise Fluent::UnrecoverableError, "Invalid path component detected in tag: #{metadata.tag}"
end

hash = {}
tag_parts = metadata.tag.split('.')
tag_parts.each_with_index do |part, i|
Expand Down Expand Up @@ -858,10 +868,21 @@ def extract_placeholders(str, chunk)
end

rvalue = rvalue.gsub(CHUNK_KEY_PLACEHOLDER_PATTERN) do |matched|
hash.fetch(matched) do
replace = hash.fetch(matched) do
log.warn "chunk key placeholder '#{matched[2..-2]}' not replaced. template:#{str}"
''
end
if replace.to_s.match?(INVALID_PATH_COMPONENT_PATTERN)
raise Fluent::UnrecoverableError, "Invalid path component detected in #{matched}: #{replace}"
end

replace
end
# Check if the number of parent directory components (../) has increased due to variable substitution
if rvalue.match?(PARENT_DIRECTORY_PATTERN)
if rvalue.scan(PARENT_DIRECTORY_PATTERN).size > str.scan(PARENT_DIRECTORY_PATTERN).size
raise Fluent::UnrecoverableError, "Invalid path component detected, replaced to: #{rvalue}"
end
end
end

Expand Down
123 changes: 123 additions & 0 deletions test/plugin/test_output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,129 @@ def waiting(seconds)
assert { logs.any? { |log| log.include?("chunk key placeholder 'key2' not replaced. template:#{tmpl}") } }
end

sub_test_case 'Path boundary validation for ${tag} placeholder' do
data(
'normal_dot' => 'app.web',
'safe_slash' => 'app/web',
'symbol_mixed' => 'my_tag-123',
'no_match' => 'app..web/log',
)
test 'allows valid tags including safe slashes' do |tag|
@i.configure(config_element('ROOT', '', {}, [config_element('buffer', 'tag')]))
t = event_time('2016-04-11 20:30:00 +0900')
v = { key1: "value1" }
m = create_metadata(timekey: t, tag: tag, variables: v)
result = @i.extract_placeholders("/data/${tag}/log", m)
assert_equal "/data/#{tag}/log", result
end

data(
'unix_relative' => '../etc/cron.d',
'windows_relative' => '..\\Windows\\System32',
'unix_absolute' => '/etc/passwd',
'windows_absolute' => '\\Windows'
)
test 'rejects tags containing parent directory relative or absolute root paths' do |tag|
@i.configure(config_element('ROOT', '', {}, [config_element('buffer', 'tag')]))
t = event_time('2016-04-11 20:30:00 +0900')
v = { key1: "value1" }
m = create_metadata(timekey: t, tag: tag, variables: v)

err = assert_raise(Fluent::UnrecoverableError) do
@i.extract_placeholders("/data/${tag}/log", m)
end
assert_match(/Invalid path component detected in tag/, err.message)
end

data(
'unix_relative' => '../etc/cron.d',
'windows_relative' => '..\\Windows\\System32',
'unix_absolute' => '/etc/passwd',
'windows_absolute' => '\\Windows'
)
test 'rejects tags containing traversal paths even when only parts are used' do |tag|
@i.configure(config_element('ROOT', '', {}, [config_element('buffer', 'tag')]))
t = event_time('2016-04-11 20:30:00 +0900')
v = { key1: "value1" }
m = create_metadata(timekey: t, tag: tag, variables: v)

err = assert_raise(Fluent::UnrecoverableError) do
@i.extract_placeholders("/data/${tag[0]}/log", m)
end
assert_match(/Invalid path component detected in tag/, err.message)
end

data(
'unix_relative' => '../etc/cron.d',
'windows_relative' => '..\\Windows\\System32',
'unix_absolute' => '/etc/passwd',
'windows_absolute' => '\\Windows'
)
test 'rejects variables containing parent directory relative or absolute root paths' do |var_value|
@i.configure(config_element('ROOT', '', {}, [config_element('buffer', 'tag,file')]))
t = event_time('2016-04-11 20:30:00 +0900')
v = { file: var_value }
m = create_metadata(timekey: t, tag: 'app.web', variables: v)

err = assert_raise(Fluent::UnrecoverableError) do
@i.extract_placeholders("/data/${tag}/${file}", m)
end
assert_match(/Invalid path component detected in \$\{file\}/, err.message)
end

data(
'combined ../' => { dot: '.', dotslash: './', file: 'safe.log' },
'combined ..\\' => { dot: '.', dotslash: '.\\', file: 'safe.log' },
)
test 'rejects combined variables containing dot and slash combination' do |var_value|
@i.configure(config_element('ROOT', '', {}, [config_element('buffer', 'tag,dot,dotslash,file')]))
t = event_time('2016-04-11 20:30:00 +0900')
v = var_value
m = create_metadata(timekey: t, tag: 'app.web', variables: v)

err = assert_raise(Fluent::UnrecoverableError) do
@i.extract_placeholders("/data/${dot}${dotslash}${file}", m)
end
assert_match(/Invalid path component detected, replaced to/, err.message)
end

test 'passes safely when placeholder expansion does NOT introduce additional ../' do
@i.configure(config_element('ROOT', '', {}, [config_element('buffer', 'val1,val2,val3')]))
t = event_time('2016-04-11 20:30:00 +0900')
v = { val1: "safe", val2: "." }
m = create_metadata(timekey: t, variables: v)

result = @i.extract_placeholders("../data/${val1}${val2}log", m)
assert_equal "../data/safe.log", result
end

data(
'normal_string' => 'app.log',
'safe_slash' => 'logs/app.log',
'integer_val' => 80,
'boolean_val' => true
)
test 'allows valid variables including safe strings and non-string types' do |var_value|
@i.configure(config_element('ROOT', '', {}, [config_element('buffer', 'tag,file')]))
t = event_time('2016-04-11 20:30:00 +0900')
v = { file: var_value }
m = create_metadata(timekey: t, tag: 'app.web', variables: v)

result = @i.extract_placeholders("/data/${tag}/${file}", m)
assert_equal "/data/app.web/#{var_value}", result
end

test 'skips validation if ${tag} placeholder is not used' do
@i.configure(config_element('ROOT', '', {}, [config_element('buffer', 'tag')]))
t = event_time('2016-04-11 20:30:00 +0900')
v = { key1: "value1" }
m = create_metadata(timekey: t, tag: '../etc/passwd', variables: v)

result = @i.extract_placeholders("/data/static/log", m)
assert_equal "/data/static/log", result
end
end

sub_test_case '#placeholder_validators' do
test 'returns validators for time, tag and keys when a template has placeholders even if plugin is not configured with these keys' do
@i.configure(config_element('ROOT', '', {}, [config_element('buffer', '')]))
Expand Down
Loading