Skip to content

Commit 4cdf2fa

Browse files
committed
Enforce a limit on the depth of the JSON object. (#35063)
1 parent bc029e8 commit 4cdf2fa

File tree

5 files changed

+100
-15
lines changed

5 files changed

+100
-15
lines changed

server/src/main/java/org/elasticsearch/index/mapper/ContentPath.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,8 @@ public String pathAsText(String name) {
6666
sb.append(name);
6767
return sb.toString();
6868
}
69+
70+
public int length() {
71+
return index;
72+
}
6973
}

server/src/main/java/org/elasticsearch/index/mapper/JsonFieldMapper.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,12 @@ private static class Defaults {
9797
FIELD_TYPE.freeze();
9898
}
9999

100+
public static final int DEPTH_LIMIT = 20;
100101
public static final int IGNORE_ABOVE = Integer.MAX_VALUE;
101102
}
102103

103104
public static class Builder extends FieldMapper.Builder<Builder, JsonFieldMapper> {
105+
private int depthLimit = Defaults.DEPTH_LIMIT;
104106
private int ignoreAbove = Defaults.IGNORE_ABOVE;
105107

106108
public Builder(String name) {
@@ -123,6 +125,14 @@ public Builder indexOptions(IndexOptions indexOptions) {
123125
return super.indexOptions(indexOptions);
124126
}
125127

128+
public Builder depthLimit(int depthLimit) {
129+
if (depthLimit < 0) {
130+
throw new IllegalArgumentException("[depth_limit] must be positive, got " + depthLimit);
131+
}
132+
this.depthLimit = depthLimit;
133+
return this;
134+
}
135+
126136
public Builder ignoreAbove(int ignoreAbove) {
127137
if (ignoreAbove < 0) {
128138
throw new IllegalArgumentException("[ignore_above] must be positive, got " + ignoreAbove);
@@ -153,7 +163,7 @@ public JsonFieldMapper build(BuilderContext context) {
153163
fieldType().setSearchAnalyzer(WHITESPACE_ANALYZER);
154164
}
155165
return new JsonFieldMapper(name, fieldType, defaultFieldType,
156-
ignoreAbove, context.indexSettings());
166+
ignoreAbove, depthLimit, context.indexSettings());
157167
}
158168
}
159169

@@ -166,7 +176,10 @@ public Mapper.Builder<?,?> parse(String name, Map<String, Object> node, ParserCo
166176
Map.Entry<String, Object> entry = iterator.next();
167177
String propName = entry.getKey();
168178
Object propNode = entry.getValue();
169-
if (propName.equals("ignore_above")) {
179+
if (propName.equals("depth_limit")) {
180+
builder.depthLimit(XContentMapValues.nodeIntegerValue(propNode, -1));
181+
iterator.remove();
182+
} else if (propName.equals("ignore_above")) {
170183
builder.ignoreAbove(XContentMapValues.nodeIntegerValue(propNode, -1));
171184
iterator.remove();
172185
} else if (propName.equals("null_value")) {
@@ -367,19 +380,22 @@ public Query wildcardQuery(String value,
367380
}
368381

369382
private final JsonFieldParser fieldParser;
383+
private int depthLimit;
370384
private int ignoreAbove;
371385

372386
private JsonFieldMapper(String simpleName,
373387
MappedFieldType fieldType,
374388
MappedFieldType defaultFieldType,
375389
int ignoreAbove,
390+
int depthLimit,
376391
Settings indexSettings) {
377392
super(simpleName, fieldType, defaultFieldType, indexSettings, MultiFields.empty(), CopyTo.empty());
378393
assert fieldType.indexOptions().compareTo(IndexOptions.DOCS_AND_FREQS) <= 0;
379394

395+
this.depthLimit = depthLimit;
380396
this.ignoreAbove = ignoreAbove;
381397
this.fieldParser = new JsonFieldParser(fieldType.name(), keyedFieldName(),
382-
ignoreAbove, fieldType.nullValueAsString());
398+
depthLimit, ignoreAbove, fieldType.nullValueAsString());
383399
}
384400

385401
@Override
@@ -453,14 +469,18 @@ protected void parseCreateField(ParseContext context, List<IndexableField> field
453469
protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException {
454470
super.doXContentBody(builder, includeDefaults, params);
455471

456-
if (includeDefaults || fieldType().nullValue() != null) {
457-
builder.field("null_value", fieldType().nullValue());
472+
if (includeDefaults || depthLimit != Defaults.DEPTH_LIMIT) {
473+
builder.field("depth_limit", depthLimit);
458474
}
459475

460476
if (includeDefaults || ignoreAbove != Defaults.IGNORE_ABOVE) {
461477
builder.field("ignore_above", ignoreAbove);
462478
}
463479

480+
if (includeDefaults || fieldType().nullValue() != null) {
481+
builder.field("null_value", fieldType().nullValue());
482+
}
483+
464484
if (includeDefaults || fieldType().splitQueriesOnWhitespace()) {
465485
builder.field("split_queries_on_whitespace", fieldType().splitQueriesOnWhitespace());
466486
}

server/src/main/java/org/elasticsearch/index/mapper/JsonFieldParser.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,18 @@ public class JsonFieldParser {
4040
private final String rootFieldName;
4141
private final String keyedFieldName;
4242

43+
private final int depthLimit;
4344
private final int ignoreAbove;
4445
private final String nullValueAsString;
4546

4647
JsonFieldParser(String rootFieldName,
4748
String keyedFieldName,
49+
int depthLimit,
4850
int ignoreAbove,
4951
String nullValueAsString) {
5052
this.rootFieldName = rootFieldName;
5153
this.keyedFieldName = keyedFieldName;
54+
this.depthLimit = depthLimit;
5255
this.ignoreAbove = ignoreAbove;
5356
this.nullValueAsString = nullValueAsString;
5457
}
@@ -104,6 +107,7 @@ private void parseFieldValue(XContentParser.Token token,
104107
List<IndexableField> fields) throws IOException {
105108
if (token == XContentParser.Token.START_OBJECT) {
106109
path.add(currentName);
110+
validateDepthLimit(path);
107111
parseObject(parser, path, fields);
108112
path.remove();
109113
} else if (token == XContentParser.Token.START_ARRAY) {
@@ -141,6 +145,13 @@ private void addField(ContentPath path,
141145
fields.add(new StringField(keyedFieldName, new BytesRef(keyedValue), Field.Store.NO));
142146
}
143147

148+
private void validateDepthLimit(ContentPath path) {
149+
if (path.length() + 1 > depthLimit) {
150+
throw new IllegalArgumentException("The provided JSON field [" + rootFieldName + "] exceeds" +
151+
" the maximum depth limit of [" + depthLimit + "].");
152+
}
153+
}
154+
144155
public static String createKeyedValue(String key, String value) {
145156
return key + SEPARATOR + value;
146157
}

server/src/test/java/org/elasticsearch/index/mapper/JsonFieldMapperTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,35 @@ public void testFieldMultiplicity() throws Exception {
300300
assertEquals(new BytesRef("key3\0false"), keyedFields[2].binaryValue());
301301
}
302302

303+
public void testDepthLimit() throws IOException {
304+
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject()
305+
.startObject("type")
306+
.startObject("properties")
307+
.startObject("field")
308+
.field("type", "json")
309+
.field("depth_limit", 2)
310+
.endObject()
311+
.endObject()
312+
.endObject()
313+
.endObject());
314+
315+
DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping));
316+
assertEquals(mapping, mapper.mappingSource().toString());
317+
318+
BytesReference doc = BytesReference.bytes(XContentFactory.jsonBuilder().startObject()
319+
.startObject("field")
320+
.startObject("key1")
321+
.startObject("key2")
322+
.field("key3", "value")
323+
.endObject()
324+
.endObject()
325+
.endObject()
326+
.endObject());
327+
328+
expectThrows(MapperParsingException.class, () ->
329+
mapper.parse(SourceToParse.source("test", "type", "1", doc, XContentType.JSON)));
330+
}
331+
303332
public void testIgnoreAbove() throws IOException {
304333
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject()
305334
.startObject("type")

server/src/test/java/org/elasticsearch/index/mapper/JsonFieldParserTests.java

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.elasticsearch.common.xcontent.XContentParser;
2828
import org.elasticsearch.common.xcontent.XContentType;
2929
import org.elasticsearch.common.xcontent.json.JsonXContent;
30-
import org.elasticsearch.index.mapper.JsonFieldMapper.RootJsonFieldType;
3130
import org.elasticsearch.test.ESTestCase;
3231
import org.elasticsearch.test.XContentTestUtils;
3332
import org.junit.Before;
@@ -41,7 +40,8 @@ public class JsonFieldParserTests extends ESTestCase {
4140
@Before
4241
public void setUp() throws Exception {
4342
super.setUp();
44-
parser = new JsonFieldParser("field", "field._keyed", Integer.MAX_VALUE, null);
43+
parser = new JsonFieldParser("field", "field._keyed",
44+
Integer.MAX_VALUE, Integer.MAX_VALUE, null);
4545
}
4646

4747
public void testTextValues() throws Exception {
@@ -213,15 +213,36 @@ public void testNestedObjects() throws Exception {
213213
assertEquals(new BytesRef("parent2.key\0value"), keyedField2.binaryValue());
214214
}
215215

216+
public void testDepthLimit() throws Exception {
217+
String input = "{ \"parent1\": { \"key\" : \"value\" }," +
218+
"\"parent2\": [{ \"key\" : { \"key\" : \"value\" }}]}";
219+
XContentParser xContentParser = createXContentParser(input);
220+
JsonFieldParser configuredParser = new JsonFieldParser("field", "field._keyed",
221+
2, Integer.MAX_VALUE, null);
222+
223+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class,
224+
() -> configuredParser.parse(xContentParser));
225+
assertEquals("The provided JSON field [field] exceeds the maximum depth limit of [2].", e.getMessage());
226+
}
227+
228+
public void testDepthLimitBoundary() throws Exception {
229+
String input = "{ \"parent1\": { \"key\" : \"value\" }," +
230+
"\"parent2\": [{ \"key\" : { \"key\" : \"value\" }}]}";
231+
XContentParser xContentParser = createXContentParser(input);
232+
JsonFieldParser configuredParser = new JsonFieldParser("field", "field._keyed",
233+
3, Integer.MAX_VALUE, null);
234+
235+
List<IndexableField> fields = configuredParser.parse(xContentParser);
236+
assertEquals(4, fields.size());
237+
}
238+
216239
public void testIgnoreAbove() throws Exception {
217240
String input = "{ \"key\": \"a longer field than usual\" }";
218241
XContentParser xContentParser = createXContentParser(input);
242+
JsonFieldParser configuredParser = new JsonFieldParser("field", "field._keyed",
243+
Integer.MAX_VALUE, 10, null);
219244

220-
RootJsonFieldType fieldType = new RootJsonFieldType();
221-
fieldType.setName("field");
222-
JsonFieldParser parserWithIgnoreAbove = new JsonFieldParser("field", "field._keyed", 10, null);
223-
224-
List<IndexableField> fields = parserWithIgnoreAbove.parse(xContentParser);
245+
List<IndexableField> fields = configuredParser.parse(xContentParser);
225246
assertEquals(0, fields.size());
226247
}
227248

@@ -233,10 +254,10 @@ public void testNullValues() throws Exception {
233254
assertEquals(0, fields.size());
234255

235256
xContentParser = createXContentParser(input);
236-
JsonFieldParser parserWithNullValue = new JsonFieldParser("field", "field._keyed",
237-
Integer.MAX_VALUE, "placeholder");
257+
JsonFieldParser configuredParser = new JsonFieldParser("field", "field._keyed",
258+
Integer.MAX_VALUE, Integer.MAX_VALUE, "placeholder");
238259

239-
fields = parserWithNullValue.parse(xContentParser);
260+
fields = configuredParser.parse(xContentParser);
240261
assertEquals(2, fields.size());
241262

242263
IndexableField field = fields.get(0);

0 commit comments

Comments
 (0)