Skip to content

out_opensearch: limit what a host placeholder can expand to - #192

Open
kenhys wants to merge 2 commits into
mainfrom
fix-host
Open

kenhys wants to merge 2 commits into
mainfrom
fix-host

Conversation

@kenhys

@kenhys kenhys commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Before: A placeholder in host/hosts was expanded and passed to the connection options as is. So a tag or a record field could add a host with ",", userinfo with "@" or a path with "/". The configured user, password and custom_headers were then sent to that host.

After: Each placeholder value is checked before the whole setting is expanded, so a value can only fill in a host name and a port. The rest of the setting, such as the "," between hosts or a scheme, userinfo and path the operator wrote, is left as it is. A placeholder used together with credentials logs a warning at startup.

Before: A placeholder in `host`/`hosts` was expanded and passed to the
connection options as is. So a tag or a record field could add a host
with ",", userinfo with "@" or a path with "/". The configured user,
password and custom_headers were then sent to that host.

After: Each placeholder value is checked before the whole setting is
expanded, so a value can only fill in a host name and a port. The rest
of the setting, such as the "," between hosts or a scheme, userinfo
and path the operator wrote, is left as it is. A placeholder used
together with credentials logs a warning at startup.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kentaro Hayashi <hayashi@clear-code.com>
@kenhys
kenhys marked this pull request as ready for review September 17, 2026 01:35
@kenhys
kenhys requested a review from Watson1978 September 17, 2026 01:35
Comment thread lib/fluent/plugin/out_opensearch.rb Outdated
# without this a log sender could add a host with ",", userinfo with "@" or a path with "/",
# and receive the configured user/password and custom_headers. A port is
# allowed because it can only be chosen on a host the value already picked.
HOST_PLACEHOLDER_VALUE_PATTERN = /\A[0-9A-Za-z_\-.]*(:[0-9]+)?\z/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This plugin supports IPv6 address https://github.com/fluent/fluent-plugin-opensearch#ipv6-addresses

However, it seems this regexp does not support ipv6.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other cases.

  1. Domain escape

    • . is allowed without limit, so a value can still move the request to another domain. With host myhost-${pipeline_id} plus user/password, a record field of evil.example.net passes this check and get_connection_options returns host: "myhost-evil.example.net" with the configured credentials attached. custom_headers follows the same path, so the leak this PR describes is still open for a template with no fixed suffix.
  2. :port accepted anywhere

    • (:[0-9]+)? is accepted wherever the placeholder sits, but a port can only follow the end of a host. hosts https://logs-${pipeline_id}.example.com/os with the value 1:9999 passes and becomes https://logs-1:9999.example.com/os, which URI() rejects. connection_options_description in the rescue of send_bulk then raises the same URI::InvalidURIError, so ignore_exceptions and exception_backup never apply.
  3. Empty value

    • * lets an empty value pass. With hosts ${pipeline_id} and an empty value, get_connection_options("") returns {hosts: []}, so the destination is silently gone instead of rejected. + would reject that, and also :9999, which currently reaches URI() and raises.

Before: a value could hold a host name and a port wherever the
placeholder was.

* `host myhost-${pipeline_id}` let a record field move the request to
  another domain
* `hosts https://logs-${pipeline_id}.example.com/os` let a value put a
  port in the middle of a name, which `URI()` cannot read
* An empty value dropped the host without an error
* An IPv6 address was rejected everywhere

After: what a value may hold depends on where the operator put the
placeholder.

* A value that continues a name the operator wrote fills in one label,
  so it can add neither a "." nor a port
* A value that stands for the whole host name may hold a name and a port
* An IPv6 address is allowed where the operator wrote the "[]" or the
  scheme that `URI()` needs to read it
* An empty value is rejected everywhere

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kentaro Hayashi <hayashi@clear-code.com>
# could add a host with ",", userinfo with "@" or a path with "/", and
# the configured user/password and custom_headers would go to that host.
HOST_PLACEHOLDER_PATTERN = /\$\{[^}]*\}/
HOST_LABEL_PATTERN = /\A[0-9A-Za-z_\-]+\z/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that host logs-${tag}.google.com is rejected once the tag contains a dot.

  • host logs-${tag}.google.com
    • tag "test" -> ACCEPT with "logs-test.google.com"
    • tag "test.template" -> REJECT

So, it needs to contain \. like HOST_NAME_PATTERN

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants