Security research · Fluent Bit

A log field became an Elasticsearch command

Fluent Bit pasted a record-derived index name into Bulk API syntax. One log record added an authenticated write or delete.

One value crossing two grammars
record["idx"]untrusted data
action line →grammar changed
POST /_bulkFluent Bit identity
Elasticsearchwrite / delete

One untrusted log record went into Fluent Bit. Its output request contained an attacker-chosen Elasticsearch action.

The added action came from a field inside the record. It traveled in the same Bulk API request as Fluent Bit's own output, over Fluent Bit's upstream connection, with Fluent Bit's Elasticsearch credentials.

I reproduced this against the official Fluent Bit v5.0.5 image. The Elasticsearch output emitted an extra index action and an extra delete action. I then sent those exact captured request bodies to Elasticsearch 8.15. Elasticsearch created the chosen document and deleted a pre-seeded document with a known ID.

The boundary crossed

Untrusted record field → Bulk action metadata → authenticated Elasticsearch write or delete.

The bug was not in HTTP headers or in Fluent Bit's normal JSON encoder. It sat in the line between them: record-derived metadata used to build an NDJSON action line.

The Bulk API is two small languages at once

Elasticsearch and OpenSearch accept batches through the Bulk API. The body is newline-delimited JSON. An action such as index or create takes two lines: one describes the operation, and the next contains the document. A delete needs only its action line (Elasticsearch Bulk API).

action metadata for document 1
source document 1
action metadata for document 2
source document 2

The newline is part of the grammar. It tells Elasticsearch where one instruction ends and another begins.

That led to the first useful question: when Fluent Bit lets a record choose an index prefix, does it encode that value as JSON data, or paste it directly into the action line?

In v5.0.5, the out_es plugin did the latter. With Logstash_Format On and Logstash_Prefix_Key $idx, Fluent Bit used a record accessor to read idx. It copied the bytes into an index buffer, inserted that buffer into a quoted _index field with %s, appended the encoded log document, and posted the finished body to /_bulk (v5.0.5 Elasticsearch output; bulk assembly).

The source document went through Fluent Bit's MessagePack-to-JSON encoder. The action metadata did not. A record value could stop being an index name and start changing the structure of the Bulk request.

A normal printable value choosing an index is documented behavior. Logstash_Prefix_Key exists so a record can override the static Logstash prefix. The security boundary is crossed when one record produces an additional operation.

The official image emitted the injected action

Source review showed a clean path, but the smallest meaningful test was to capture what the release binary actually sent.

I ran the ARM64 image cr.fluentbit.io/fluent/fluent-bit:5.0.5 with a Forward input and an Elasticsearch output using a record-derived prefix. The output pointed to a raw receiver that saved the HTTP body without rewriting it. The containers ran on an isolated network and the Forward port remained loopback-only.

The baseline used a printable value to select the documented index prefix. The next case made Fluent Bit emit an additional index operation with its own source line. The final case introduced a delete operation for a known index and document ID. The proof kept Fluent Bit's original document aligned with a small leading action and a continuation action. Each malicious record therefore became three valid Bulk actions, one of which was the attacker-chosen write or delete.

I ran all three Elasticsearch cases three times. A Bulk grammar validator accepted every captured body. The sibling out_opensearch plugin produced the same structures in one run per case.

Then I removed Logstash_Prefix_Key and sent the same records. The suspicious value stayed inside the ordinary JSON document. Fluent Bit escaped it, and the receiver saw one normal action under the static logstash-YYYY.MM.DD index. That control tied the behavior to action-metadata formatting rather than Forward parsing or the document encoder.

Elasticsearch created one document and deleted another

A custom grammar validator is still only a model of Elasticsearch. The next test was the real boundary: would Elasticsearch execute the exact bodies emitted by the official image?

I started Elasticsearch 8.15, submitted the captured bodies unchanged, and checked the resulting state.

The forged-write request returned errors:false. A refreshed search of the chosen index returned the test document carried in the injected action.

For the delete case, I first created a document named critical-audit-record-12345 in a test audit index. The captured request returned result:"deleted". A GET afterward returned found:false and HTTP 404.

The backend test had Elasticsearch security disabled. It answered one narrow question: were the captured bytes merely parser-shaped, or were they real Bulk operations? They were real operations.

In production, Elasticsearch or OpenSearch authorization still decides whether they succeed. The injection does not bypass the search cluster's access controls. It borrows Fluent Bit's authority. An output role limited to creating documents in one log index limits the damage. A role with broad write access can index, replace, or delete documents in every index covered by that role (Elasticsearch privileges). A useful delete also requires the target document's index and ID.

A sender trusted to submit one log can cause additional writes or deletes wherever Fluent Bit itself is allowed to perform them.

The same mistake appeared in five places

The tested prefix path was not the only consumer. Source review found five places where record data entered a quoted Bulk action field without action-line encoding:

PluginSettingFieldValidation
ElasticsearchLogstash_Prefix_Key_indexOfficial image and real Elasticsearch
ElasticsearchId_Key_idSource and upstream tests
OpenSearchLogstash_Prefix_Key_indexOfficial image and capture server
OpenSearchRecord accessor in Index_indexSource and upstream tests
OpenSearchId_Key_idSource and upstream tests

I independently ran the two Logstash_Prefix_Key paths. The other three share the same source-level mistake and appear in Fluent Bit's accepted regression suite, but I did not run them as separate release-binary cases (upstream regression commit).

This is why filtering one prefix copy would not have closed the issue. Filtering HTTP headers could never help either: the bytes were already inside the request body.

The primary classification is CWE-116, Improper Encoding or Escaping of Output. “NDJSON action-line injection” describes how that mistake becomes a database command. CWE-93 captures the newline delimiter mechanism, but CWE-116 better describes the boundary Fluent Bit failed to enforce.

Exposure depends on both sides of Fluent Bit

This configuration is not present in every Fluent Bit deployment. The tested Elasticsearch path requires Logstash_Format On and a configured Logstash_Prefix_Key. The selected field must be attacker-controlled, the record must route to the affected output, and the output identity must have permission for the added operation.

The input side matters separately. An enabled Forward input listens on 0.0.0.0:24224 with no shared key by default. A client that can reach that listener can submit records without credentials. Operators can configure a shared key and users, in which case the sender must pass that handshake first (Forward input documentation). Other input plugins have different trust boundaries.

Severity

CVSS 3.1 7.5 High — AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N.

Integrity is the demonstrated impact. The tested primitive writes and deletes; it does not read data back to the sender, so confidentiality is none. I did not establish a service outage or repeatable batch-loss condition, so availability is also none.

A shared search cluster controlled under a separate security authority can justify an 8.6 changed-scope environmental reading. I would not use that as the headline score. An authenticated input changes the privilege assumption, and a tightly scoped append-only output role reduces the practical impact.

Fluent Bit fixed all five sinks

Fluent Bit merged PR #11967, out_es/out_opensearch: sanitize bulk action metadata, on June 24, 2026. The patch added validators for record-derived action metadata and applied them to all five paths. It rejects line breaks, quotes, backslashes, and other control bytes before they can enter an action line. The regression suite includes an attempted forged delete and asserts that only one action remains.

The fix shipped in v5.0.8. Fluent Bit backported it through PR #11993 to v4.2.6. Operators should upgrade to those versions or, preferably, the latest supported release.

As of October 13, 2026, I found no matching Fluent Bit advisory or CVE in the project's public security advisories. The public fix and tests still say exactly what happened: record-derived data entered Bulk action metadata, and upstream added a five-path boundary to stop it.

A JSON encoder protects only the value that passes through it. Fluent Bit encoded each log document correctly while building the command immediately above it as a format string. One untrusted field crossed that line and became an Elasticsearch command.

Research and writing by Luke Martin, Parameno Engineering Partners — lmartin@paramenoeng.com. Independent re-verification covered the official v5.0.5 image, both output plugins, negative controls, and execution of the captured bodies by Elasticsearch 8.15.