{"record":{"id":"a221f1d71de5f078","repo":"fluent/fluentd","slug":"invalid-path-component-detected-in-tag-metadata","errorCode":null,"errorMessage":"Invalid path component detected in tag: #{metadata.tag}","messagePattern":"Invalid path component detected in tag: #(.+?)","errorType":"exception","errorClass":"Fluent::UnrecoverableError","httpStatus":null,"severity":"error","filePath":"lib/fluent/plugin/output.rb","lineNumber":832,"sourceCode":"          str.sub(CHUNK_ID_PLACEHOLDER_PATTERN) {\n            if chunk_passed\n              dump_unique_id_hex(chunk.unique_id)\n            else\n              log.warn \"${chunk_id} is not allowed in this plugin. Pass Chunk instead of metadata in extract_placeholders's 2nd argument\"\n            end\n          }\n        else\n          rvalue = str.dup\n          # strftime formatting\n          if @chunk_key_time # this section MUST be earlier than rest to use raw 'str'\n            @output_time_formatter_cache[str] ||= Fluent::Timezone.formatter(@timekey_zone, str)\n            rvalue = @output_time_formatter_cache[str].call(metadata.timekey)\n          end\n          # ${tag}, ${tag[0]}, ${tag[1]}, ... , ${tag[-2]}, ${tag[-1]}\n          if @chunk_key_tag\n            if str.include?('${tag}')\n              if metadata.tag.match?(INVALID_PATH_COMPONENT_PATTERN)\n                raise Fluent::UnrecoverableError, \"Invalid path component detected in tag: #{metadata.tag}\"\n              end\n\n              rvalue = rvalue.gsub('${tag}', metadata.tag)\n            end\n            if CHUNK_TAG_PLACEHOLDER_PATTERN.match?(str)\n              if metadata.tag.match?(INVALID_PATH_COMPONENT_PATTERN)\n                raise Fluent::UnrecoverableError, \"Invalid path component detected in tag: #{metadata.tag}\"\n              end\n\n              hash = {}\n              tag_parts = metadata.tag.split('.')\n              tag_parts.each_with_index do |part, i|\n                hash[\"${tag[#{i}]}\"] = part\n                hash[\"${tag[#{i-tag_parts.size}]}\"] = part\n              end\n              rvalue = rvalue.gsub(CHUNK_TAG_PLACEHOLDER_PATTERN, hash)\n            end\n            if rvalue =~ CHUNK_TAG_PLACEHOLDER_PATTERN","sourceCodeStart":814,"sourceCodeEnd":850,"githubUrl":"https://github.com/fluent/fluentd/blob/dd45c6e18dc7be33b5e5a0f0767bf46307ff5626/lib/fluent/plugin/output.rb#L814-L850","documentation":"At flush time (not configure time) `extract_placeholders` substitutes `${tag}` into a path and rejects tags matching `INVALID_PATH_COMPONENT_PATTERN = /\\.\\.[\\/\\\\]|^[\\/\\\\]/` — i.e. containing `../` (or `..\\`) or starting with `/` or `\\`. This blocks path traversal outside the target directory. It raises `Fluent::UnrecoverableError`, so the chunk is not retried: it goes to `<secondary>` if configured, otherwise it is discarded.","triggerScenarios":"An output with `<buffer tag>` and a path containing `${tag}` (e.g. `path /logs/${tag}/app.log`) receives a record whose tag is `foo/../../etc`, `..%2F`-adjacent forms that decode to traversal, or an absolute-looking `/var/log/x`. The regex matches on `metadata.tag` at output.rb:832-835 and the unrecoverable error propagates from the flush.","commonSituations":"Ingesting user-controlled tags (webhook names, container/job names, syslog program names) into file paths; `rewrite_tag_filter` or record fields promoted to tags without sanitization; multi-tenant forwarders where tenants pick their own tag.","solutions":["Sanitize tags at ingress: strip/replace `..`, leading `/`, and backslashes via `record_transformer`/`rewrite_tag_filter` or a custom parser before they reach tag-chunked outputs","Configure `<secondary> @type secondary_file` so traversal-tagged chunks are captured to disk instead of dropped","Avoid `${tag}` in paths for untrusted tags — chunk on a sanitized record field (`${key}`) validated upstream"],"exampleFix":"# before\n<match **>\n  @type file\n  path /logs/${tag}/app.log\n  <buffer tag>\n    @type file\n  </buffer>\n</match>\n\n# after (sanitize tag at rewrite time + keep a local safety net)\n<match raw.**>\n  @type rewrite_tag_filter\n  <rule>\n    key message\n    pattern ^.*/.*$\n    tag safe.${tag}\n  </rule>\n</match>\n<match **>\n  @type file\n  path /logs/${tag}/app.log\n  <buffer tag>\n    @type file\n  </buffer>\n  <secondary>\n    @type secondary_file\n    directory /var/log/fluent/backup\n  </secondary>\n</match>","handlingStrategy":"fallback","validationCode":"INVALID = %r{\\.\\.[/\\\\]|^[/\\\\]}\ntag = event_meta.tag\nwarn_and_retag(tag) if tag.match?(INVALID)\n# In config form, sanitize at ingress:\n# <filter **> @type record_transformer <record> tag ${tag.gsub('..', '__')} </record> </filter>","typeGuard":"def safe_tag_for_path?(tag)\n  !tag.match?(%r{\\.\\.[/\\\\]|^[/\\\\]})\nend","tryCatchPattern":"# in a custom output wrapping path construction:\nbegin\n  path = extract_placeholders(@path, chunk)\nrescue Fluent::UnrecoverableError => e\n  log.error \"tag rejected by path traversal guard\", error: e\n  # let it propagate so the chunk routes to <secondary>; do not rescue-and-drop\n  raise\nend","preventionTips":["Never feed unvalidated external strings (usernames, job ids, webhook names) into tags that reach ${tag} paths","Configure <secondary> @type secondary_file so traversal-tagged chunks are preserved, not dropped","Sanitize at the edge: strip '..' segments, leading '/', and backslashes when tags are created (rewrite_tag_filter, custom parser)","Alert on UnrecoverableError in logs — each one is data diverted from its destination"],"tags":["fluentd","path-traversal","tag","security","runtime"],"backgroundTag":"path-traversal-blocked","analyzedSha":"dd45c6e18dc7be33b5e5a0f0767bf46307ff5626","analyzedAt":"2026-08-21T16:22:07.332Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}