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
227 changes: 70 additions & 157 deletions lib/fluent/vulnerability_checker.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
# frozen_string_literal: true

require 'yaml'

require 'fluent/config'
require 'fluent/env'

module Fluent
class VulnerabilityChecker
CHUNK_ID_PLACEHOLDER_PATTERN = /\$\{chunk_id\}/
PLACEHOLDER_PATTERN = /\$\{.*?\}/
MESSAGES_PATH = File.join(__dir__, 'vulnerability_checker', 'messages.yaml')

class Rule
def initialize(plugin_categories, plugin_name = nil, &condition_proc)
attr_reader :cve

def initialize(cve, plugin_categories, plugin_name = nil, &condition_proc)
@cve = cve
@plugin_categories = Array(plugin_categories)
@plugin_name = plugin_name
@condition_proc = condition_proc
end

def register_report_message(&report_proc)
@report_proc = report_proc
def register_state_message(&state_proc)
@state_proc = state_proc
self
end

def report(element)
@report_proc&.call(element)
def state(element)
@state_proc&.call(element)
end

def safe?(element)
Expand All @@ -33,184 +39,91 @@ def safe?(element)
end
end

class Report
# Width of the "* State: " label. Continuation lines are aligned with it.
STATE_INDENT = ' ' * 14
# Width of the " * EN: " label.
TEXT_INDENT = ' ' * 8

def initialize(messages_path = MESSAGES_PATH)
@messages = YAML.load_file(messages_path)
end

def render(rule, element)
message = @messages.fetch(rule.cve)
state = rule.state(element)

lines = [
"# #{message['title']}",
"* Severity: #{message['severity']}",
"* CVSS Score: #{message['cvss']}",
"* Location: <#{element.name}> - @type #{element['@type']}"
]
if state
lines << "* State: #{state}"
lines << "#{STATE_INDENT}#{align(message['note'], STATE_INDENT)}" if message['note']
end
lines << '* Description:'
lines.concat(bilingual(message['description']))
lines << '* Workaround:'
lines.concat(bilingual(message['workaround']))
lines.join("\n")
end

private

def bilingual(texts)
['en', 'ja'].map { |lang| " * #{lang.upcase}: #{align(texts.fetch(lang), TEXT_INDENT)}" }
end

def align(text, indent)
text.strip.gsub("\n", "\n#{indent}")
end
end

attr_reader :vulnerabilities # for test

def initialize
@vulnerabilities = 0
@report = Report.new
@rules = []

# Fluentd: RCE via ${tag} in file in output/buffer/secondary/store sections
@rules << Rule.new(['match', 'buffer', 'secondary', 'store'], 'file') { |element|
@rules << Rule.new('CVE-2026-44024', ['match', 'buffer', 'secondary', 'store'], 'file') { |element|
# replace the safe ${chunk_id} with an empty string to clear it, and check if the remaining string contains ${...}
element['path']&.gsub(CHUNK_ID_PLACEHOLDER_PATTERN, '')&.match?(PLACEHOLDER_PATTERN)
}.register_report_message do |element|
<<~"MSG"
# Remote Code Execution (RCE) via Arbitrary File Write in Dynamic Placeholder
* Severity: Critical
* CVSS Score: 10.0
* Location: <#{element.name}> - @type #{element['@type']}
* State: path = '#{element['path']}'
* Description:
* EN: If the ${tag} placeholder is used in the path parameter of the file output plugin, there is a risk that
an attacker could write files to arbitrary paths, potentially leading to RCE.
* JA: pathでプレースホルダーにtagを指定している場合、設定ファイルの書き方や実行権限が不適切だと、任意のパスに
ファイルを書き込まれ、リモートからコード実行可能になる危険があります。
* Workaround:
* EN: If you cannot immediately update the package, apply mitigations such as restricting access,
running with non-root privileges, modifying the configuration file, or removing untrusted tags.
* JA: すぐにパッケージを更新できない場合、アクセスの制限、非root権限での実行、設定ファイルの修正、信頼できない
tagを除去するなどの緩和策を適用してください。
MSG
end
}.register_state_message { |element| "path = '#{element['path']}'" }

# Fluentd: RCE via ${tag} in secondary_file
@rules << Rule.new('secondary', 'secondary_file') { |element|
@rules << Rule.new('CVE-2026-44024', 'secondary', 'secondary_file') { |element|
# replace the safe ${chunk_id} with an empty string to clear it, and check if the remaining string contains ${...}
element['directory']&.gsub(CHUNK_ID_PLACEHOLDER_PATTERN, '')&.match?(PLACEHOLDER_PATTERN) ||
element['basename']&.gsub(CHUNK_ID_PLACEHOLDER_PATTERN, '')&.match?(PLACEHOLDER_PATTERN)
}.register_report_message do |element|
state_msg = ['directory', 'basename'].map { |k| "#{k} = '#{element[k]}'" if element[k]&.match?(PLACEHOLDER_PATTERN) }.compact.join(', ')
<<~"MSG"
# Remote Code Execution (RCE) via Arbitrary File Write in Dynamic Placeholder
* Severity: Critical
* CVSS Score: 10.0
* Location: <#{element.name}> - @type #{element['@type']}
* State: #{state_msg}
* Description:
* EN: If the ${tag} placeholder is used in the directory or basename parameter of the secondary_file plugin,
there is a risk that an attacker could write files to arbitrary paths, potentially leading to RCE.
* JA: directoryまたはbasenameでプレースホルダーにtagを指定している場合、設定ファイルの書き方や実行権限が不適切だと、任意のパスに
ファイルを書き込まれ、リモートからコード実行可能になる危険があります。
* Workaround:
* EN: If you cannot immediately update the package, apply mitigations such as restricting access,
running with non-root privileges, modifying the configuration file, or removing untrusted tags.
* JA: すぐにパッケージを更新できない場合、アクセスの制限、非root権限での実行、設定ファイルの修正、信頼できない
tagを除去するなどの緩和策を適用してください。
MSG
}.register_state_message do |element|
['directory', 'basename'].map { |k| "#{k} = '#{element[k]}'" if element[k]&.match?(PLACEHOLDER_PATTERN) }.compact.join(', ')
end

# Fluentd: Exposure of Sensitive Information
@rules << Rule.new('source', 'monitor_agent') { |element| element['bind'].nil? || element['bind'] == '0.0.0.0' }.register_report_message do |element|
<<~"MSG"
# Exposure of Sensitive Information via Monitor Agent API
* Severity: High
* CVSS Score: 7.5
* Location: <#{element.name}> - @type #{element['@type']}
* State: bind = '#{element['bind'] || 'default'}'.
(Note: In Fluentd 1.19.2 and earlier, the default value of bind is 0.0.0.0.)
* Description:
* EN: monitor_agent is exposed externally, allowing attackers to read Fluentd configuration details via its API.
* JA: monitor_agentによって公開されているAPIを悪用して、Fluentdの設定内容を読み取られる危険性があります。
* Workaround:
* EN: If you cannot immediately update the package, apply mitigations such as restricting access,
or allowing access only from localhost.
* JA: すぐにパッケージを更新できない場合、アクセスの制限、ローカルホストからのみのアクセスを受け付けるなどの
緩和策を適用してください。
MSG
end
@rules << Rule.new('CVE-2026-44025', 'source', 'monitor_agent') { |element|
element['bind'].nil? || element['bind'] == '0.0.0.0'
}.register_state_message { |element| "bind = '#{element['bind'] || 'default'}'." }

# Fluentd: DoS via Gzip Decompression in http input plugin
@rules << Rule.new('source', 'http') { |element| true }.register_report_message do |element|
<<~"MSG"
# Denial of Service (DoS) via Gzip Decompression Bomb
* Severity: High
* CVSS Score: 7.5
* Location: <#{element.name}> - @type #{element['@type']}
* Description:
* EN: Sending maliciously crafted gzip-compressed data to Fluentd can consume excessive memory during
decompression, potentially causing it to crash.
* JA: 細工したgzip圧縮データをFluentdへと送りつけることで、展開時に大量のメモリを消費させてFluentdをクラッシュ
する危険性があります。
* Workaround:
* EN: If you cannot immediately update the package, apply mitigations such as restricting access, or enforcing
size limits when decompressing content using a reverse proxy.
* JA: すぐにパッケージを更新できない場合、アクセスの制限、リバースプロキシ等を用いたGZIP展開時のサイズ制限等の
緩和策を適用してください。
MSG
end
@rules << Rule.new('CVE-2026-44160', 'source', 'http') { |element| true }

# Fluentd: DoS via Gzip Decompression in forward input plugin
@rules << Rule.new('source', 'forward') { |element| true }.register_report_message do |element|
<<~"MSG"
# Denial of Service (DoS) via Gzip Decompression Bomb
* Severity: High
* CVSS Score: 7.5
* Location: <#{element.name}> - @type #{element['@type']}
* Description:
* EN: Sending maliciously crafted gzip-compressed data to Fluentd can consume excessive memory during
decompression, potentially causing it to crash.
* JA: 細工したgzip圧縮データを受信することで、展開時に大量のメモリを消費してFluentdがクラッシュする危険性が
あります。
* Workaround:
* EN: If you cannot immediately update the package, apply mitigations such as restricting access, or enforcing
size limits when decompressing content using a reverse proxy.
* JA: すぐにパッケージを更新できない場合、アクセスの制限、リバースプロキシ等を用いたGZIP展開時のサイズ制限等の
緩和策を適用してください。
MSG
end
@rules << Rule.new('CVE-2026-44160', 'source', 'forward') { |element| true }

# fluent-plugin-s3: DoS via Gzip Decompression
@rules << Rule.new('source', 's3') { |element| true }.register_report_message do |element|
<<~"MSG"
# Denial of Service (DoS) via Gzip Decompression Bomb
* Severity: Low
* CVSS Score: 2.7
* Location: <#{element.name}> - @type #{element['@type']}
* Description:
* EN: Sending maliciously crafted gzip-compressed data from S3 to Fluentd can consume excessive memory when
processing requests, potentially causing it to crash.
* JA: 細工したgzip圧縮データをS3から受信することで、展開時に大量のメモリを消費してFluentdがクラッシュする
危険性があります。
* Workaround:
* EN: If you cannot immediately update the package, apply mitigations such as restricting access, or enforcing
size limits when decompressing content using a reverse proxy.
* JA: すぐにパッケージを更新できない場合、アクセスの制限、リバースプロキシ等を用いたGZIP展開時のサイズ制限等の
緩和策を適用してください。
MSG
end
@rules << Rule.new('CVE-2026-44162', 'source', 's3') { |element| true }

# fluent-plugin-opentelemetry: DoS
@rules << Rule.new('source', 'opentelemetry') { |element| true }.register_report_message do |element|
<<~"MSG"
# Denial of Service (DoS) via Gzip Decompression Bomb or Malicious HTTP Requests
* Severity: Moderate
* CVSS Score: 5.3
* Location: <#{element.name}> - @type #{element['@type']}
* Description:
* EN: Sending maliciously crafted gzip-compressed data to Fluentd can consume excessive memory when processing
requests, potentially causing it to crash.
* JA: 細工したgzip圧縮データやHTTPリクエストを受信することで、リクエスト処理時に大量のメモリを消費してFluentdが
クラッシュする危険性があります。
* Workaround:
* EN: If you cannot immediately update the package, apply mitigations such as restricting access, or enforcing
size limits when processing requests using a reverse proxy.
* JA: すぐにパッケージを更新できない場合、アクセスの制限、リバースプロキシ等を用いたリクエストサイズ制限等の
緩和策を適用してください。
MSG
end
@rules << Rule.new('CVE-2026-44163', 'source', 'opentelemetry') { |element| true }

# Fluentd: SSRF via ${endpoint} in http output plugin
@rules << Rule.new('match', 'http') { |element| element['endpoint']&.match?(%r[https?://[^/]*\$\{]) }.register_report_message do |element|
<<~"MSG"
# Server-Side Request Forgery (SSRF) via Placeholder Expansion in out_http
* Severity: Moderate
* CVSS Score: 5.8
* Location: <#{element.name}> - @type #{element['@type']}
* Description:
* EN: When out_http determines destination servers for data transmission based on tags, this vulnerability
allows attackers to manipulate data transmission by sending crafted data, resulting in data being sent
to servers other than the intended ones.
* JA: out_httpでデータを別のサーバーに送る際の送信先をタグから決定している場合、細工したデータを送りつけられると、
本来の意図とは異なるサーバーへとデータを送信してしまうことが可能な脆弱性です。
* Workaround:
* EN: If you cannot immediately update the package, apply mitigation measures such as:
* not dynamically setting hostnames with placeholders
* restricting access from untrusted networks
* allowing only specific hosts
* JA: すぐにパッケージを更新できない場合、プレースホルダーでホスト名を動的に設定しない、信頼できない
ネットワークからのアクセスの制限、特定のホストのみ許可するなどの緩和策を適用してください。
MSG
end
@rules << Rule.new('CVE-2026-44161', 'match', 'http') { |element|
element['endpoint']&.match?(%r[https?://[^/]*\$\{])
}
end

def validate(config_path)
Expand All @@ -227,7 +140,7 @@ def check_element(element)
@rules.each do |rule|
unless rule.safe?(element)
@vulnerabilities += 1
puts rule.report(element)
puts @report.render(rule, element)
puts "=" * 115
end
end
Expand Down
Loading