Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Guard reserved tags field against incorrect use #14822

Merged
merged 31 commits into from
Jan 25, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
58ac5c0
This commit guards reserved `tags` field from setting by event parsin…
kaisecheng Dec 22, 2022
410c8b9
guard `tags` from setting a key/value map though add_field
kaisecheng Dec 22, 2022
461a34d
Merge branch 'main' of github.com:elastic/logstash into guard_reserve…
kaisecheng Dec 22, 2022
97dce2e
typo
kaisecheng Dec 22, 2022
a528472
fix unit test
kaisecheng Dec 22, 2022
9cf92f2
add flag `--event_api.tags.illegal` to control behavior of illegal va…
kaisecheng Jan 3, 2023
8348a04
add doc
kaisecheng Jan 3, 2023
f89d765
fix tests
kaisecheng Jan 4, 2023
fa1ee92
fix incorrect use of FieldReference
kaisecheng Jan 4, 2023
d37d453
rename illegal FieldReference
kaisecheng Jan 4, 2023
1e1a951
Update logstash-core/locales/en.yml
kaisecheng Jan 10, 2023
c356d9f
Update logstash-core/locales/en.yml
kaisecheng Jan 10, 2023
4d7c080
Update logstash-core/src/test/java/org/logstash/EventTest.java
kaisecheng Jan 10, 2023
f3ab6ed
Update logstash-core/src/test/java/org/logstash/EventTest.java
kaisecheng Jan 10, 2023
9fa7993
support type checking to guard against string and array of string
kaisecheng Jan 11, 2023
94607c0
Merge branch 'main' of github.com:elastic/logstash into guard_reserve…
kaisecheng Jan 11, 2023
713923c
add integration test
kaisecheng Jan 12, 2023
909526b
fix unit test
kaisecheng Jan 12, 2023
409b634
Update logstash-core/locales/en.yml
kaisecheng Jan 19, 2023
4e322d1
Update logstash-core/lib/logstash/runner.rb
kaisecheng Jan 19, 2023
3ffa3cc
Update docs/static/settings-file.asciidoc
kaisecheng Jan 19, 2023
61de3d0
Update logstash-core/src/main/java/org/logstash/Event.java
kaisecheng Jan 19, 2023
ad4259d
add FieldReference rebase
kaisecheng Jan 19, 2023
fce4f4a
update comment
kaisecheng Jan 23, 2023
cd27ed8
fix doclint
kaisecheng Jan 23, 2023
b312284
- rename illegal value to _tags instead of making a list of illegal h…
kaisecheng Jan 24, 2023
32a7d8b
Rename and clean up
kaisecheng Jan 24, 2023
05ae484
clean up rebaseOnto
kaisecheng Jan 24, 2023
11bf4aa
Update logstash-core/src/main/java/org/logstash/FieldReference.java
kaisecheng Jan 24, 2023
ff21c0d
Update docs/static/settings-file.asciidoc
kaisecheng Jan 24, 2023
5df8219
update docs
kaisecheng Jan 24, 2023
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
8 changes: 4 additions & 4 deletions docker/data/logstash/env2yaml/env2yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
// Merge environment variables into logstash.yml.
// For example, running Docker with:
//
// docker run -e pipeline.workers=6
// docker run -e pipeline.workers=6
//
// or
//
// docker run -e PIPELINE_WORKERS=6
// docker run -e PIPELINE_WORKERS=6
//
// will cause logstash.yml to contain the line:
//
// pipeline.workers: 6
//
// pipeline.workers: 6
package main

import (
Expand Down Expand Up @@ -72,6 +71,7 @@ func normalizeSetting(setting string) (string, error) {
"config.debug",
"config.support_escapes",
"config.field_reference.escape_style",
"event_api.tags.illegal",
"queue.type",
"path.queue",
"queue.page_capacity",
Expand Down
6 changes: 6 additions & 0 deletions docs/static/settings-file.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -346,4 +346,10 @@ separating each log lines per pipeline could be helpful in case you need to trou
| `allow_superuser`
| Setting to `true` to allow or `false` to block running Logstash as a superuser.
| `true`

| `event_api.tags.illegal`
| When set to `warn`, allow illegal value assignment to the reserved `tags` field.
When set to `rename`, illegal value in `tags` will be moved to `_tags`. A tag `_tagsparsefailure` is added to `tags` field to indicate the illegal assignment.
kaisecheng marked this conversation as resolved.
Show resolved Hide resolved
This flag will be deprecated in the next major release and the `tags` field will only allow a string or an array of string.
kaisecheng marked this conversation as resolved.
Show resolved Hide resolved
| `rename`
|=======================================================================
1 change: 1 addition & 0 deletions logstash-core/lib/logstash/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ module Environment
Setting::TimeValue.new("config.reload.interval", "3s"), # in seconds
Setting::Boolean.new("config.support_escapes", false),
Setting::String.new("config.field_reference.escape_style", "none", true, %w(none percent ampersand)),
Setting::String.new("event_api.tags.illegal", "rename", true, %w(rename warn)),
Setting::Boolean.new("metric.collect", true),
Setting::String.new("metric.timers", "delayed", true, %w(delayed live)),
Setting::String.new("pipeline.id", "main"),
Expand Down
11 changes: 11 additions & 0 deletions logstash-core/lib/logstash/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ class LogStash::Runner < Clamp::StrictCommand
:default => LogStash::SETTINGS.get_default("config.field_reference.escape_style"),
:attribute_name => "config.field_reference.escape_style"

option ["--event_api.tags.illegal"], "STRING",
I18n.t("logstash.runner.flag.event_api.tags.illegal"),
:default => LogStash::SETTINGS.get_default("event_api.tags.illegal"),
:attribute_name => "event_api.tags.illegal"

# Module settings
option ["--modules"], "MODULES",
I18n.t("logstash.runner.flag.modules"),
Expand Down Expand Up @@ -337,6 +342,12 @@ def execute
logger.debug("Setting global FieldReference escape style: #{field_reference_escape_style}")
org.logstash.FieldReference::set_escape_style(field_reference_escape_style)

tags_illegal_setting = settings.get_setting('event_api.tags.illegal').value
if tags_illegal_setting == 'warn'
logger.warn(I18n.t("logstash.runner.tags-illegal-warning"))
kaisecheng marked this conversation as resolved.
Show resolved Hide resolved
org.logstash.Event::set_illegal_tags_action(tags_illegal_setting)
end

return start_shell(setting("interactive"), binding) if setting("interactive")

module_parser = LogStash::Modules::CLIParser.new(setting("modules_list"), setting("modules_variable_list"))
Expand Down
16 changes: 16 additions & 0 deletions logstash-core/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ en:
configtest-flag-information: |-
You may be interested in the '--configtest' flag which you can use to validate
logstash's configuration before you choose to restart a running system.
tags-illegal-warning: >-
Setting `event_api.tags.illegal` to `warn` allows illegal values in the reserved `tags` field, which may crash pipeline unexpectedly.
This flag will be deprecated in the next major release and `tags` field will only allow a string or an array of string.
kaisecheng marked this conversation as resolved.
Show resolved Hide resolved
# YAML named reference to the logstash.runner.configuration
# so we can later alias it from logstash.agent.configuration
configuration: &runner_configuration
Expand Down Expand Up @@ -252,6 +255,19 @@ en:
HTML-style ampersand-hash encoding notation
representing decimal unicode codepoints
(`[` is `&#91;`; `]` is `&#93;`).
event_api:
tags:
illegal: |+
The reserved field `tags` allow string and string[] since v7.9.
Assigning other types could crash the pipeline. This allows you to
change the behavior when `tags` get assigned with illegal type, eg. map.
kaisecheng marked this conversation as resolved.
Show resolved Hide resolved

Available options are:
- `rename`: illegal value in `tags` will be moved to `_tags`.
A tag `_tagsparsefailure` is added to `tags` field to
indicate the illegal assignment. This is the default option.
- `warn`: allow illegal value assignment and print warning
at startup.
kaisecheng marked this conversation as resolved.
Show resolved Hide resolved
modules: |+
Load Logstash modules.
Modules can be defined using multiple instances
Expand Down
8 changes: 4 additions & 4 deletions logstash-core/spec/logstash/filters/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,8 @@ def filter(event)
}
CONFIG

sample_one("type" => "noop", "go" => "away", "tags" => {"blackhole" => "go"}) do
expect(subject.get("[tags][blackhole]")).to eq("go")
sample_one("type" => "noop", "go" => "away", "tags" => "blackhole") do
expect(subject.get("[tags]")).to eq("blackhole")
end

end
Expand All @@ -393,8 +393,8 @@ def filter(event)
}
CONFIG

sample_one("type" => "noop", "tags" => {"blackhole" => "go"}) do
expect(subject.get("[tags][blackhole]")).to eq("go")
sample_one("type" => "noop", "tags" => "blackhole") do
expect(subject.get("[tags]")).to eq("blackhole")
end
end
end
Expand Down
44 changes: 42 additions & 2 deletions logstash-core/src/main/java/org/logstash/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
Expand All @@ -62,8 +63,14 @@ public final class Event implements Cloneable, Queueable, co.elastic.logstash.ap
public static final String VERSION_ONE = "1";
private static final String DATA_MAP_KEY = "DATA";
private static final String META_MAP_KEY = "META";
public static final String TAGS = "tags";
public static final String TAGS_FAILURE_TAG = "_tagsparsefailure";
public static final String TAGS_FAILURE_FIELD = "_tags";

private static final FieldReference TAGS_FIELD = FieldReference.from("tags");
enum IllegalTagsAction { RENAME, WARN }
private static IllegalTagsAction ILLEGAL_TAGS_ACTION = IllegalTagsAction.RENAME;

private static final FieldReference TAGS_FIELD = FieldReference.from(TAGS);

private static final Logger logger = LogManager.getLogger(Event.class);

Expand Down Expand Up @@ -106,6 +113,15 @@ public Event(ConvertedMap data) {
}
this.cancelled = false;

// guard tags field from key/value map, only string or list is allowed
if (ILLEGAL_TAGS_ACTION == IllegalTagsAction.RENAME) {
final Object tags = Accessors.get(data, TAGS_FIELD);
if (tags instanceof ConvertedMap) {
Copy link
Member

Choose a reason for hiding this comment

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

instead of a block-list of one particular bad shape, can we have an allow-list of the supported shapes?

this.setField(TAGS_FAILURE_FIELD, tags);
initTag(TAGS_FAILURE_TAG);
}
}

Object providedTimestamp = data.get(TIMESTAMP);
// keep reference to the parsedTimestamp for tagging below
Timestamp parsedTimestamp = initTimestamp(providedTimestamp);
Expand Down Expand Up @@ -209,8 +225,28 @@ public void setField(final FieldReference field, final Object value) {
Accessors.set(metadata, field, Valuefier.convert(value));
break;
default:
Accessors.set(data, field, Valuefier.convert(value));
final FieldReference renamedField = renameIllegalTags(field);
Accessors.set(data, renamedField, Valuefier.convert(value));
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure this does what we want, since it renames the field independently of the shape of its value.

It may be worth intercepting TAGS_FIELD to be handled by a helper method:

    public void setField(final FieldReference field, final Object value) {
        if (field.equals(TAGS_FIELD)) {
            setTagsField(value);
            return;
        }
        switch (field.type()) {
            // ...
        }
    }
    private void setTagsField(final Object value)
        if (isLegalTagValue(value)) {
            setField(TAGS_FIELD, value);
        } else if (ILLEGAL_TAGS_ACTION == IllegalTagsAction.RENAME)
            setField(FieldReference.from(TAGS_FAILURE_FIELD), value);
        } else {
            // log warning? avoid flooding though?
            setField(TAGS_FIELD, value);
        }
    }

    private boolean isLegalTagValue(final Object value) {
        if (value instanceof String) { return true; }
        if (value instanceof List) { return true; } // TODO: make sure it is a list of _strings_, which is difficult to do with generics and type erasure

        return false;
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Applied the suggestion. It now handles the illegal tags field first in Event.setField()

}
}

/**
* if the field is a reserved `tags` field with map structure,
* construct a FieldReference pointing to `_tag` field and add _tagsparsefailure to `tags` field,
* otherwise return the original field.
* @param field
* @return Renamed {@link FieldReference} or original field
*/
private FieldReference renameIllegalTags(final FieldReference field) {
if (ILLEGAL_TAGS_ACTION == IllegalTagsAction.RENAME &&
field.getPath() != null && field.getPath().length > 0 && field.getPath()[0].equals(TAGS)) {
Copy link
Member

Choose a reason for hiding this comment

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

I read this as:

  • IF we are supposed to rename illegal-shaped tags
  • AND the field reference has a non-empty path
  • AND the first-level is tags

In my mind, we need to handle two cases when ILLEGAL_TAGS_ACTION == IllegalTagsAction.RENAME:

  • (path == null || path.length == 0) && key.equals(TAGS), with a value that is neither a string nor an array of strings (setting top-level tags to be an illegal shape)
  • (path != null && path.length > 0 && path[0].equals(TAGS)) regardless of value (setting field nested inside the tags, which cannot have nested fields in it)

Examples that should trigger a rename using the Event.setField(String, Object):

  • event.setField("[tags]", Map.of("poison", "true"))
  • event.setField("[tags][poison]", "true")

Similarly, using the Ruby APIs Event#set:

  • event.set("[tags]", { "poison" => "true"})
  • event.set("[tags][poison]", "true")

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for listing the cases. The PR is changed to handle other illegal types, not limited to map. And _tags field holds a list of illegal values.

tag(TAGS_FAILURE_TAG);
String[] failTagsPath = Arrays.copyOf(field.getPath(), field.getPath().length);
failTagsPath[0] = TAGS_FAILURE_FIELD;
return new FieldReference(failTagsPath, field.getKey(), field.type());
}

return field;
}

@Override
Expand Down Expand Up @@ -495,6 +531,10 @@ private void scalarTagFallback(final String existing, final String tag) {
appendTag(tags, tag);
}

public static void setIllegalTagsAction(final String action) {
ILLEGAL_TAGS_ACTION = IllegalTagsAction.valueOf(action.toUpperCase());
}

@Override
public byte[] serialize() throws JsonProcessingException {
final Map<String, Map<String, Object>> map = new HashMap<>(2, 1.0F);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public static void setEscapeStyle(final String escapeStyleSpec) {
*/
private final int type;

private FieldReference(final String[] path, final String key, final int type) {
public FieldReference(final String[] path, final String key, final int type) {
Copy link
Member

Choose a reason for hiding this comment

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

I am wary of making this public, since it would allow plugins built with the Java plugin API to route around our field reference caching.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I revert it to private and build the FieldReference from string instead

this.key = ConvertedMap.internStringForUseAsKey(key);
ConvertedMap.internStringsForUseAsKeys(path);
this.path = path;
Expand Down
49 changes: 49 additions & 0 deletions logstash-core/src/test/java/org/logstash/EventTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -516,4 +516,53 @@ public void removeMetadata() {
assertTrue(event.getMetadata().isEmpty());
assertFalse(event.includes("[@metadata][foo]"));
}

@Test
public void setTopLevelTagsWithMap() {
Event.setIllegalTagsAction(Event.IllegalTagsAction.RENAME.toString());
final Event event = new Event();
event.setField("[tags][foo]", "bar");

assertEquals(event.getField(Event.TAGS), Collections.singletonList(Event.TAGS_FAILURE_TAG));
assertEquals(event.getField("[_tags][foo]"), "bar");
}

@Test
public void createEventWithTopLevelTagsWithMap() {
Event.setIllegalTagsAction(Event.IllegalTagsAction.RENAME.toString());
Map<String, Object> inner = new HashMap<>();
inner.put("poison", "true");
Map<String, Object> data = new HashMap<>();
data.put("tags", inner);
final Event event = new Event(data);

assertEquals(event.getField(Event.TAGS), Collections.singletonList(Event.TAGS_FAILURE_TAG));
assertEquals(event.getField("[_tags][poison]"), "true");
}


@Test
public void allowTopLevelTagsWithMap() {
Event.setIllegalTagsAction(Event.IllegalTagsAction.WARN.toString());
final Event event = new Event();
event.setField("[tags][foo]", "bar");
Event.setIllegalTagsAction(Event.IllegalTagsAction.RENAME.toString());

assertNull(event.getField(Event.TAGS_FAILURE_FIELD));
assertEquals(event.getField("[tags][foo]"), "bar");
}

kaisecheng marked this conversation as resolved.
Show resolved Hide resolved
@Test
public void allowCreatingEventWithTopLevelTagsWithMap() {
Event.setIllegalTagsAction(Event.IllegalTagsAction.WARN.toString());
Map<String, Object> inner = new HashMap<>();
inner.put("poison", "true");
Map<String, Object> data = new HashMap<>();
data.put("tags", inner);
final Event event = new Event(data);
Event.setIllegalTagsAction(Event.IllegalTagsAction.RENAME.toString());

assertNull(event.getField(Event.TAGS_FAILURE_FIELD));
assertEquals(event.getField("[tags][poison]"), "true");
}
kaisecheng marked this conversation as resolved.
Show resolved Hide resolved
}