Skip to content

Commit 58308a0

Browse files
committed
Add stricter geohash parsing (#30376)
Adds verification that geohashes are not empty and contain only valid characters. It fixes the issue when en empty geohash is treated as [-180, -90] and geohashes with non-geohash character are getting resolved into invalid coordinates. Closes #23579
1 parent 6cfb3e7 commit 58308a0

File tree

7 files changed

+120
-28
lines changed

7 files changed

+120
-28
lines changed

docs/CHANGELOG.asciidoc

+3
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ analysis module. ({pull}30397[#30397])
105105

106106
{ref-64}/breaking_64_api_changes.html#copy-source-settings-on-resize[Allow copying source settings on index resize operations] ({pull}30255[#30255])
107107

108+
Geo::
109+
* Add validation that geohashes are not empty and don't contain unsupported characters ({pull}30376[#30376])
110+
108111
Rollup::
109112
* Validate timezone in range queries to ensure they match the selected job when
110113
searching ({pull}30338[#30338])

server/src/main/java/org/elasticsearch/common/geo/GeoHashUtils.java

+6
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,17 @@ public static final String stringEncodeFromMortonLong(long hashedVal, final int
171171
* Encode to a morton long value from a given geohash string
172172
*/
173173
public static final long mortonEncode(final String hash) {
174+
if (hash.isEmpty()) {
175+
throw new IllegalArgumentException("empty geohash");
176+
}
174177
int level = 11;
175178
long b;
176179
long l = 0L;
177180
for(char c : hash.toCharArray()) {
178181
b = (long)(BASE_32_STRING.indexOf(c));
182+
if (b < 0) {
183+
throw new IllegalArgumentException("unsupported symbol [" + c + "] in geohash [" + hash + "]");
184+
}
179185
l |= (b<<((level--*5) + MORTON_OFFSET));
180186
if (level < 0) {
181187
// We cannot handle more than 12 levels

server/src/main/java/org/elasticsearch/common/geo/GeoPoint.java

+6-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import org.elasticsearch.common.xcontent.ToXContentFragment;
2929
import org.elasticsearch.common.xcontent.XContentBuilder;
3030
import org.elasticsearch.ElasticsearchParseException;
31-
import org.elasticsearch.common.Strings;
3231

3332
import java.io.IOException;
3433
import java.util.Arrays;
@@ -126,7 +125,12 @@ public GeoPoint resetFromIndexableField(IndexableField field) {
126125
}
127126

128127
public GeoPoint resetFromGeoHash(String geohash) {
129-
final long hash = mortonEncode(geohash);
128+
final long hash;
129+
try {
130+
hash = mortonEncode(geohash);
131+
} catch (IllegalArgumentException ex) {
132+
throw new ElasticsearchParseException(ex.getMessage(), ex);
133+
}
130134
return this.reset(GeoHashUtils.decodeLatitude(hash), GeoHashUtils.decodeLongitude(hash));
131135
}
132136

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

+33-25
Original file line numberDiff line numberDiff line change
@@ -300,14 +300,7 @@ public Mapper parse(ParseContext context) throws IOException {
300300
if (token == XContentParser.Token.START_ARRAY) {
301301
// its an array of array of lon/lat [ [1.2, 1.3], [1.4, 1.5] ]
302302
while (token != XContentParser.Token.END_ARRAY) {
303-
try {
304-
parse(context, GeoUtils.parseGeoPoint(context.parser(), sparse));
305-
} catch (ElasticsearchParseException e) {
306-
if (ignoreMalformed.value() == false) {
307-
throw e;
308-
}
309-
context.addIgnoredField(fieldType.name());
310-
}
303+
parseGeoPointIgnoringMalformed(context, sparse);
311304
token = context.parser().nextToken();
312305
}
313306
} else {
@@ -327,42 +320,57 @@ public Mapper parse(ParseContext context) throws IOException {
327320
} else {
328321
while (token != XContentParser.Token.END_ARRAY) {
329322
if (token == XContentParser.Token.VALUE_STRING) {
330-
parse(context, sparse.resetFromString(context.parser().text(), ignoreZValue.value()));
323+
parseGeoPointStringIgnoringMalformed(context, sparse);
331324
} else {
332-
try {
333-
parse(context, GeoUtils.parseGeoPoint(context.parser(), sparse));
334-
} catch (ElasticsearchParseException e) {
335-
if (ignoreMalformed.value() == false) {
336-
throw e;
337-
}
338-
}
325+
parseGeoPointIgnoringMalformed(context, sparse);
339326
}
340327
token = context.parser().nextToken();
341328
}
342329
}
343330
}
344331
} else if (token == XContentParser.Token.VALUE_STRING) {
345-
parse(context, sparse.resetFromString(context.parser().text(), ignoreZValue.value()));
332+
parseGeoPointStringIgnoringMalformed(context, sparse);
346333
} else if (token == XContentParser.Token.VALUE_NULL) {
347334
if (fieldType.nullValue() != null) {
348335
parse(context, (GeoPoint) fieldType.nullValue());
349336
}
350337
} else {
351-
try {
352-
parse(context, GeoUtils.parseGeoPoint(context.parser(), sparse));
353-
} catch (ElasticsearchParseException e) {
354-
if (ignoreMalformed.value() == false) {
355-
throw e;
356-
}
357-
context.addIgnoredField(fieldType.name());
358-
}
338+
parseGeoPointIgnoringMalformed(context, sparse);
359339
}
360340
}
361341

362342
context.path().remove();
363343
return null;
364344
}
365345

346+
/**
347+
* Parses geopoint represented as an object or an array, ignores malformed geopoints if needed
348+
*/
349+
private void parseGeoPointIgnoringMalformed(ParseContext context, GeoPoint sparse) throws IOException {
350+
try {
351+
parse(context, GeoUtils.parseGeoPoint(context.parser(), sparse));
352+
} catch (ElasticsearchParseException e) {
353+
if (ignoreMalformed.value() == false) {
354+
throw e;
355+
}
356+
context.addIgnoredField(fieldType.name());
357+
}
358+
}
359+
360+
/**
361+
* Parses geopoint represented as a string and ignores malformed geopoints if needed
362+
*/
363+
private void parseGeoPointStringIgnoringMalformed(ParseContext context, GeoPoint sparse) throws IOException {
364+
try {
365+
parse(context, sparse.resetFromString(context.parser().text(), ignoreZValue.value()));
366+
} catch (ElasticsearchParseException e) {
367+
if (ignoreMalformed.value() == false) {
368+
throw e;
369+
}
370+
context.addIgnoredField(fieldType.name());
371+
}
372+
}
373+
366374
@Override
367375
protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException {
368376
super.doXContentBody(builder, includeDefaults, params);

server/src/test/java/org/elasticsearch/common/geo/GeoHashTests.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.elasticsearch.common.geo;
2020

2121
import org.apache.lucene.geo.Rectangle;
22+
import org.elasticsearch.ElasticsearchParseException;
2223
import org.elasticsearch.test.ESTestCase;
2324

2425
/**
@@ -95,7 +96,17 @@ public void testLongGeohashes() {
9596
Rectangle expectedBbox = GeoHashUtils.bbox(geohash);
9697
Rectangle actualBbox = GeoHashUtils.bbox(extendedGeohash);
9798
assertEquals("Additional data points above 12 should be ignored [" + extendedGeohash + "]" , expectedBbox, actualBbox);
98-
9999
}
100100
}
101+
102+
public void testInvalidGeohashes() {
103+
IllegalArgumentException ex;
104+
105+
ex = expectThrows(IllegalArgumentException.class, () -> GeoHashUtils.mortonEncode("55.5"));
106+
assertEquals("unsupported symbol [.] in geohash [55.5]", ex.getMessage());
107+
108+
ex = expectThrows(IllegalArgumentException.class, () -> GeoHashUtils.mortonEncode(""));
109+
assertEquals("empty geohash", ex.getMessage());
110+
}
111+
101112
}

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

+47
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import static org.hamcrest.Matchers.instanceOf;
5050
import static org.hamcrest.Matchers.not;
5151
import static org.hamcrest.Matchers.notNullValue;
52+
import static org.hamcrest.Matchers.nullValue;
5253

5354
public class GeoPointFieldMapperTests extends ESSingleNodeTestCase {
5455

@@ -398,4 +399,50 @@ public void testNullValue() throws Exception {
398399
assertThat(defaultValue, not(equalTo(doc.rootDoc().getField("location").binaryValue())));
399400
}
400401

402+
public void testInvalidGeohashIgnored() throws Exception {
403+
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
404+
.startObject("properties")
405+
.startObject("location")
406+
.field("type", "geo_point")
407+
.field("ignore_malformed", "true")
408+
.endObject()
409+
.endObject().endObject().endObject());
410+
411+
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
412+
.parse("type", new CompressedXContent(mapping));
413+
414+
ParsedDocument doc = defaultMapper.parse(SourceToParse.source("test", "type", "1", BytesReference
415+
.bytes(XContentFactory.jsonBuilder()
416+
.startObject()
417+
.field("location", "1234.333")
418+
.endObject()),
419+
XContentType.JSON));
420+
421+
assertThat(doc.rootDoc().getField("location"), nullValue());
422+
}
423+
424+
425+
public void testInvalidGeohashNotIgnored() throws Exception {
426+
String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type")
427+
.startObject("properties")
428+
.startObject("location")
429+
.field("type", "geo_point")
430+
.endObject()
431+
.endObject().endObject().endObject());
432+
433+
DocumentMapper defaultMapper = createIndex("test").mapperService().documentMapperParser()
434+
.parse("type", new CompressedXContent(mapping));
435+
436+
MapperParsingException ex = expectThrows(MapperParsingException.class,
437+
() -> defaultMapper.parse(SourceToParse.source("test", "type", "1", BytesReference
438+
.bytes(XContentFactory.jsonBuilder()
439+
.startObject()
440+
.field("location", "1234.333")
441+
.endObject()),
442+
XContentType.JSON)));
443+
444+
assertThat(ex.getMessage(), equalTo("failed to parse"));
445+
assertThat(ex.getRootCause().getMessage(), equalTo("unsupported symbol [.] in geohash [1234.333]"));
446+
}
447+
401448
}

server/src/test/java/org/elasticsearch/index/search/geo/GeoPointParsingTests.java

+13
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,19 @@ public void testInvalidField() throws IOException {
175175
assertThat(e.getMessage(), is("field must be either [lat], [lon] or [geohash]"));
176176
}
177177

178+
public void testInvalidGeoHash() throws IOException {
179+
XContentBuilder content = JsonXContent.contentBuilder();
180+
content.startObject();
181+
content.field("geohash", "!!!!");
182+
content.endObject();
183+
184+
XContentParser parser = createParser(JsonXContent.jsonXContent, BytesReference.bytes(content));
185+
parser.nextToken();
186+
187+
Exception e = expectThrows(ElasticsearchParseException.class, () -> GeoUtils.parseGeoPoint(parser));
188+
assertThat(e.getMessage(), is("unsupported symbol [!] in geohash [!!!!]"));
189+
}
190+
178191
private XContentParser objectLatLon(double lat, double lon) throws IOException {
179192
XContentBuilder content = JsonXContent.contentBuilder();
180193
content.startObject();

0 commit comments

Comments
 (0)