Skip to content

Commit c60f0b3

Browse files
authored
Fix crash of pipeline due to NPE when lookup in DB with custom fields (#225)
Permit the user to handle an unexpected error (through event tag) instead of crashing the pipeline, when the data lookup encounter a customised field in the DB. Protect the handleEvent method from NPE generated in the MAxMind GeoIP reader library. That NPE happens when fetching data that contains customised attributes, this PR protects from NPE and return a lookup error so that the Event can be tagged with failure.
1 parent d622e3b commit c60f0b3

15 files changed

+86
-15
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 7.3.1
2+
- Avoid to crash pipelines when lookup a database with customised fields. [#225](https://github.com/logstash-plugins/logstash-filter-geoip/pull/225)
3+
14
## 7.3.0
25
- Added support for MaxMind GeoIP2 Enterprise and Anonymous-IP databases ([#223](https://github.com/logstash-plugins/logstash-filter-geoip/pull/223))
36
- Updated MaxMind dependencies.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
7.3.0
1+
7.3.1

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ plugins {
4040
group "org.logstash.filters"
4141
version "${new File("VERSION").text.trim()}"
4242

43-
String junitVersion = '5.9.2'
43+
String junitVersion = '5.11.2' // at least 5.11 is needed to use @FieldSource with @ParameterizedTest
4444
String maxmindGeoip2Version = '2.17.0'
4545
String maxmindDbVersion = '2.1.0'
4646
String log4jVersion = '2.17.1'

lib/logstash-filter-geoip_jars.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
require 'jar_dependencies'
44
require_jar('com.maxmind.geoip2', 'geoip2', '2.17.0')
55
require_jar('com.maxmind.db', 'maxmind-db', '2.1.0')
6-
require_jar('org.logstash.filters', 'logstash-filter-geoip', '7.3.0')
6+
require_jar('org.logstash.filters', 'logstash-filter-geoip', '7.3.1')

spec/filters/geoip_ecs_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
let(:common_options) { {"source" => "message", "database" => CITYDB, "target" => target} }
2323

2424
before(:each) do
25-
allow_any_instance_of(described_class).to receive(:ecs_compatibility).and_return(ecs_compatibility)
25+
allow(plugin).to receive(:ecs_compatibility).and_return(ecs_compatibility)
2626
plugin.register
2727
end
2828

@@ -170,7 +170,7 @@
170170

171171
context "ECS disabled" do
172172
before do
173-
allow_any_instance_of(described_class).to receive(:ecs_compatibility).and_return(:disabled)
173+
allow(plugin).to receive(:ecs_compatibility).and_return(:disabled)
174174
plugin.register
175175
plugin.filter(event)
176176
end
@@ -193,7 +193,7 @@
193193

194194
context "ECS mode" do
195195
before do
196-
allow_any_instance_of(described_class).to receive(:ecs_compatibility).and_return(:v1)
196+
allow(plugin).to receive(:ecs_compatibility).and_return(:v1)
197197
end
198198

199199
context "`target` is unset" do

spec/filters/geoip_online_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def setup_filter(db_path)
6363
end
6464

6565
before(:each) do
66-
allow_any_instance_of(described_class).to receive(:load_database_manager?).and_return(true)
66+
allow(plugin).to receive(:load_database_manager?).and_return(true)
6767
stub_const("LogStash::Filters::Geoip::DatabaseManager", double("DatabaseManager.Class", :instance => mock_manager))
6868
end
6969

src/main/java/org/logstash/filters/geoip/GeoIPFilter.java

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@
4848
import java.util.stream.Collectors;
4949

5050
public class GeoIPFilter implements Closeable {
51+
52+
// This exception could raise during the processing of datapoint with custom fields, check out
53+
// for more details https://github.com/logstash-plugins/logstash-filter-geoip/issues/226
54+
static class GeoIp2InvalidCustomFieldException extends GeoIp2Exception {
55+
public GeoIp2InvalidCustomFieldException(Throwable cause) {
56+
super("invalid custom field", cause);
57+
}
58+
}
59+
5160
private static final Logger logger = LogManager.getLogger();
5261
private final String sourceField;
5362
private final String targetField;
@@ -152,7 +161,7 @@ public boolean handleEvent(RubyEvent rubyEvent) {
152161
throw new IllegalArgumentException("Expected input field value to be String or List type");
153162
}
154163

155-
if (ip.trim().isEmpty()){
164+
if (ip.trim().isEmpty()) {
156165
return false;
157166
}
158167

@@ -224,7 +233,12 @@ private boolean applyGeoData(Map<Field, Object> geoData, Event event) {
224233
}
225234

226235
private Map<Field,Object> retrieveCityGeoData(InetAddress ipAddress) throws GeoIp2Exception, IOException {
227-
CityResponse response = databaseReader.city(ipAddress);
236+
CityResponse response;
237+
try {
238+
response = databaseReader.city(ipAddress);
239+
} catch (NullPointerException e) {
240+
throw new GeoIp2InvalidCustomFieldException(e);
241+
}
228242
Country country = response.getCountry();
229243
City city = response.getCity();
230244
Location location = response.getLocation();
@@ -337,7 +351,12 @@ private Map<Field,Object> retrieveCityGeoData(InetAddress ipAddress) throws GeoI
337351
}
338352

339353
private Map<Field,Object> retrieveCountryGeoData(InetAddress ipAddress) throws GeoIp2Exception, IOException {
340-
CountryResponse response = databaseReader.country(ipAddress);
354+
CountryResponse response;
355+
try {
356+
response = databaseReader.country(ipAddress);
357+
} catch (NullPointerException e) {
358+
throw new GeoIp2InvalidCustomFieldException(e);
359+
}
341360
Country country = response.getCountry();
342361
Continent continent = response.getContinent();
343362
Map<Field, Object> geoData = new EnumMap<>(Field.class);
@@ -372,7 +391,12 @@ private Map<Field,Object> retrieveCountryGeoData(InetAddress ipAddress) throws G
372391
}
373392

374393
private Map<Field, Object> retrieveIspGeoData(InetAddress ipAddress) throws GeoIp2Exception, IOException {
375-
IspResponse response = databaseReader.isp(ipAddress);
394+
IspResponse response;
395+
try {
396+
response = databaseReader.isp(ipAddress);
397+
} catch (NullPointerException e) {
398+
throw new GeoIp2InvalidCustomFieldException(e);
399+
}
376400

377401
Map<Field, Object> geoData = new EnumMap<>(Field.class);
378402
for (Field desiredField : this.desiredFields) {
@@ -411,7 +435,12 @@ private Map<Field, Object> retrieveIspGeoData(InetAddress ipAddress) throws GeoI
411435
}
412436

413437
private Map<Field, Object> retrieveAsnGeoData(InetAddress ipAddress) throws GeoIp2Exception, IOException {
414-
AsnResponse response = databaseReader.asn(ipAddress);
438+
AsnResponse response;
439+
try {
440+
response = databaseReader.asn(ipAddress);
441+
} catch (NullPointerException e) {
442+
throw new GeoIp2InvalidCustomFieldException(e);
443+
}
415444
Network network = response.getNetwork();
416445

417446
Map<Field, Object> geoData = new EnumMap<>(Field.class);
@@ -444,7 +473,12 @@ private Map<Field, Object> retrieveAsnGeoData(InetAddress ipAddress) throws GeoI
444473
}
445474

446475
private Map<Field, Object> retrieveDomainGeoData(InetAddress ipAddress) throws GeoIp2Exception, IOException {
447-
DomainResponse response = databaseReader.domain(ipAddress);
476+
DomainResponse response;
477+
try {
478+
response = databaseReader.domain(ipAddress);
479+
} catch (NullPointerException e) {
480+
throw new GeoIp2InvalidCustomFieldException(e);
481+
}
448482
Map<Field, Object> geoData = new EnumMap<>(Field.class);
449483
for (Field desiredField : this.desiredFields) {
450484
switch (desiredField) {
@@ -459,7 +493,12 @@ private Map<Field, Object> retrieveDomainGeoData(InetAddress ipAddress) throws G
459493
}
460494

461495
private Map<Field, Object> retrieveEnterpriseGeoData(InetAddress ipAddress) throws GeoIp2Exception, IOException {
462-
EnterpriseResponse response = databaseReader.enterprise(ipAddress);
496+
EnterpriseResponse response;
497+
try {
498+
response = databaseReader.enterprise(ipAddress);
499+
} catch (NullPointerException e) {
500+
throw new GeoIp2InvalidCustomFieldException(e);
501+
}
463502

464503
Map<Field, Object> geoData = new EnumMap<>(Field.class);
465504
Country country = response.getCountry();
@@ -567,7 +606,12 @@ private Map<Field, Object> retrieveEnterpriseGeoData(InetAddress ipAddress) thro
567606
}
568607

569608
private Map<Field, Object> retrieveAnonymousIpGeoData(final InetAddress ipAddress) throws GeoIp2Exception, IOException {
570-
AnonymousIpResponse response = databaseReader.anonymousIp(ipAddress);
609+
AnonymousIpResponse response;
610+
try {
611+
response = databaseReader.anonymousIp(ipAddress);
612+
} catch (NullPointerException e) {
613+
throw new GeoIp2InvalidCustomFieldException(e);
614+
}
571615

572616
Map<Field, Object> geoData = new EnumMap<>(Field.class);
573617
boolean isHostingProvider = response.isHostingProvider();

src/test/java/org/logstash/filters/geoip/GeoIPFilterTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.junit.jupiter.api.Test;
44
import org.junit.jupiter.params.ParameterizedTest;
5+
import org.junit.jupiter.params.provider.FieldSource;
56
import org.junit.jupiter.params.provider.ValueSource;
67
import org.logstash.Event;
78

@@ -15,6 +16,7 @@
1516

1617
import static org.junit.jupiter.api.Assertions.assertEquals;
1718
import static org.junit.jupiter.api.Assertions.assertTrue;
19+
import static org.junit.jupiter.api.Assertions.assertFalse;
1820
import static org.logstash.RubyUtil.RUBY;
1921
import static org.logstash.ext.JrubyEventExtLibrary.RubyEvent;
2022

@@ -24,6 +26,12 @@ class GeoIPFilterTest {
2426
private static final String SOURCE_FIELD = "ip";
2527
private static final String TARGET_FIELD = "data";
2628

29+
// used as parameters of givenDatabaseWithCustomizedFieldWhenItsAccessedTheCustomizedIPShouldntThrowAnyErrorAndReportTheLookupAsFailure
30+
@SuppressWarnings("unused")
31+
static Path[] GEO_DATABASES = {MaxMindDatabases.GEOIP2_COUNTRY, MaxMindDatabases.GEOIP2_ANONYMOUS_IP,
32+
MaxMindDatabases.GEOIP2_ENTERPRISE, MaxMindDatabases.GEOIP2_ISP,
33+
MaxMindDatabases.GEOLITE2_ASN};
34+
2735
@ParameterizedTest
2836
@ValueSource(booleans = {true, false})
2937
void handleEventWithGeoIp2CityDatabaseShouldProperlyCreateEvent(boolean ecsEnabled) {
@@ -265,6 +273,22 @@ void handleEventWithNoCustomFieldsShouldUseDatabasesDefaultFields() {
265273
}
266274
}
267275

276+
@ParameterizedTest
277+
@FieldSource("GEO_DATABASES")
278+
void givenDatabaseWithCustomizedFieldWhenItsAccessedTheCustomizedIPShouldntThrowAnyErrorAndReportTheLookupAsFailure(Path geoDatabase) {
279+
try (final GeoIPFilter filter = createFilter(geoDatabase, true, Collections.emptyList())) {
280+
final RubyEvent rubyEvent = createRubyEvent("216.160.83.60");
281+
assertFalse(filter.handleEvent(rubyEvent), "Lookup of data point with invalid custom fields should report as failed");
282+
}
283+
}
284+
@Test
285+
void givenDomainDatabaseWithCustomizedFieldWhenItsAccessedTheCustomizedIPShouldntThrowAnyErrorAndReportTheLookupAsFailure() {
286+
try (final GeoIPFilter filter = createFilter(MaxMindDatabases.GEOIP2_DOMAIN, true, Collections.emptyList())) {
287+
final RubyEvent rubyEvent = createRubyEvent("216.160.83.60");
288+
assertTrue(filter.handleEvent(rubyEvent), "Lookup of data point with invalid custom fields should report as failed");
289+
}
290+
}
291+
268292
@Test
269293
void handleEventWithListSourceFieldShouldParseFirstIp() {
270294
try (final GeoIPFilter filter = createFilter(MaxMindDatabases.GEOIP2_COUNTRY, true, Collections.emptyList())) {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)