{"record":{"id":"a28dc8368c47a41c","repo":"fluent/fluentd","slug":"invalid-path-component-detected-replaced-to-rv","errorCode":null,"errorMessage":"Invalid path component detected, replaced to: #{rvalue}","messagePattern":"Invalid path component detected, replaced to: #(.+?)","errorType":"exception","errorClass":"Fluent::UnrecoverableError","httpStatus":null,"severity":"critical","filePath":"lib/fluent/plugin/output.rb","lineNumber":885,"sourceCode":"            @chunk_keys.each do |key|\n              hash[\"${#{key}}\"] = metadata.variables[key.to_sym]\n            end\n\n            rvalue = rvalue.gsub(CHUNK_KEY_PLACEHOLDER_PATTERN) do |matched|\n              replace = hash.fetch(matched) do\n                log.warn \"chunk key placeholder '#{matched[2..-2]}' not replaced. template:#{str}\"\n                ''\n              end\n              if replace.to_s.match?(INVALID_PATH_COMPONENT_PATTERN)\n                raise Fluent::UnrecoverableError, \"Invalid path component detected in #{matched}: #{replace}\"\n              end\n\n              replace\n            end\n            # Check if the number of parent directory components (../) has increased due to variable substitution\n            if rvalue.match?(PARENT_DIRECTORY_PATTERN)\n              if rvalue.scan(PARENT_DIRECTORY_PATTERN).size > str.scan(PARENT_DIRECTORY_PATTERN).size\n                raise Fluent::UnrecoverableError, \"Invalid path component detected, replaced to: #{rvalue}\"\n              end\n            end\n          end\n\n          if rvalue =~ CHUNK_KEY_PLACEHOLDER_PATTERN\n            log.warn \"chunk key placeholder '#{$1}' not replaced. template:#{str}\"\n          end\n\n          rvalue\n        end\n      end\n\n      def emit_events(tag, es)\n        # actually this method will be overwritten by #configure\n        if @buffering\n          emit_buffered(tag, es)\n        else\n          emit_sync(tag, es)","sourceCodeStart":867,"sourceCodeEnd":903,"githubUrl":"https://github.com/fluent/fluentd/blob/dd45c6e18dc7be33b5e5a0f0767bf46307ff5626/lib/fluent/plugin/output.rb#L867-L903","documentation":"Companion guard to error 220 in Output#extract_placeholders: after all placeholder substitution, it rescans the finished path (rvalue) with PARENT_DIRECTORY_PATTERN (%r{\\.\\.[/\\\\]}) and raises Fluent::UnrecoverableError if the '../' count grew compared to the original template (str). This catches indirect escapes that the per-value check misses, e.g. a substituted value that is exactly '..' (no trailing slash) combining with adjacent template text to form extra '../' segments. As an UnrecoverableError it discards the affected buffer chunk.","triggerScenarios":"Template path /srv/${a}/../share with a record where chunk key a='..' yields /srv/../../share (2 vs 1 '../' occurrences) and raises. Also values like 'x..' or '..y' inside templates that already contain '../' when substitution increases the count.","commonSituations":"Buffer path templates that legitimately contain '..' segments combined with user-controlled chunk keys; symlink-style path templates ('/var/log/${svc}/current') fed with values like '..'; post-upgrade configs that previously resolved outside the buffer directory without complaint.","solutions":["Remove '..' segments from the path template itself and use absolute, flat buffer paths.","Reject or rewrite chunk-key values equal to '..' or containing '..' in a filter before buffering (record['a'] == '..' ? '_' : record['a']).","Replace user-controlled path segments with ${chunk_id} or a computed digest.","Add a config-time lint: assert that path.scan(%r{\\.\\.[/\\\\]}).size plus any substituted value's count never exceeds the template's."],"exampleFix":"# before\npath /var/log/fluent/${tenant}/../archive\n# after\npath /var/log/fluent/archive/${tenant}","handlingStrategy":"validation","validationCode":"# count guard mirroring the core check: substituted value must not add '../' segments\nPARENT = %r{\\.\\.[/\\\\]}\nraise_if_escape = value.scan(PARENT).size > 0 || value == '..'\n# rewrite instead of raising:\nvalue = '_' if value == '..'\nvalue = value.gsub(PARENT, '_')","typeGuard":"def safe_template_value?(v)\n  v.is_a?(String) && v != '..' && !v.match?(%r{\\.\\.[/\\\\]})\nend","tryCatchPattern":"rescue Fluent::UnrecoverableError => e\n  log.error 'chunk discarded, path escapes buffer dir', replaced: e.message\n  # ship the raw record to a fallback label for manual repair","preventionTips":["Keep buffer path templates free of '..' segments entirely.","Whitelist chunk-key values to a known-safe set when they map to directories.","Test configs with adversarial values ('..', 'a/../b', '/abs') before rollout."],"tags":["fluentd","buffer","path-traversal","unrecoverable-error","output-plugin"],"backgroundTag":"path-traversal-sanitization","analyzedSha":"dd45c6e18dc7be33b5e5a0f0767bf46307ff5626","analyzedAt":"2026-08-21T16:22:07.332Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}