diff options
author | Belen Pruvost <belen.pruvost@sonarsource.com> | 2021-04-14 15:31:51 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2021-04-14 20:03:29 +0000 |
commit | 2ac0f065bea21dda018d33e3b39ea9a4291da0ce (patch) | |
tree | f740f44cdc88981c03056faeb7c688af45f81288 /server/sonar-server-common | |
parent | 0e3231774e88bf7c76855c13d952deeab7351f1d (diff) | |
download | sonarqube-2ac0f065bea21dda018d33e3b39ea9a4291da0ce.tar.gz sonarqube-2ac0f065bea21dda018d33e3b39ea9a4291da0ce.zip |
SONAR-14682 - Filtering local network interfaces on webhooks
Diffstat (limited to 'server/sonar-server-common')
6 files changed, 152 insertions, 6 deletions
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/webhook/NetworkInterfaceProvider.java b/server/sonar-server-common/src/main/java/org/sonar/server/webhook/NetworkInterfaceProvider.java new file mode 100644 index 00000000000..a501809e680 --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/webhook/NetworkInterfaceProvider.java @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.webhook; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.server.ServerSide; + +@ServerSide +@ComputeEngineSide +public class NetworkInterfaceProvider { + + public List<InetAddress> getNetworkInterfaceAddresses() throws SocketException { + return Collections.list(NetworkInterface.getNetworkInterfaces()) + .stream() + .flatMap(ni -> Collections.list(ni.getInetAddresses()).stream()) + .collect(Collectors.toList()); + } +} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookCustomDns.java b/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookCustomDns.java index 688cfdb9bd0..71c4448c747 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookCustomDns.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookCustomDns.java @@ -20,6 +20,7 @@ package org.sonar.server.webhook; import java.net.InetAddress; +import java.net.SocketException; import java.net.UnknownHostException; import java.util.Collections; import java.util.List; @@ -36,9 +37,11 @@ import static org.sonar.process.ProcessProperties.Property.SONAR_VALIDATE_WEBHOO public class WebhookCustomDns implements Dns { private final Configuration configuration; + private final NetworkInterfaceProvider networkInterfaceProvider; - public WebhookCustomDns(Configuration configuration) { + public WebhookCustomDns(Configuration configuration, NetworkInterfaceProvider networkInterfaceProvider) { this.configuration = configuration; + this.networkInterfaceProvider = networkInterfaceProvider; } @NotNull @@ -46,10 +49,19 @@ public class WebhookCustomDns implements Dns { public List<InetAddress> lookup(@NotNull String host) throws UnknownHostException { InetAddress address = InetAddress.getByName(host); if (configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey()).orElse(true) - && (address.isLoopbackAddress() || address.isAnyLocalAddress())) { + && (address.isLoopbackAddress() || address.isAnyLocalAddress() || isLocalAddress(address))) { throw new IllegalArgumentException("Invalid URL: loopback and wildcard addresses are not allowed for webhooks."); } return Collections.singletonList(address); } + private boolean isLocalAddress(InetAddress address) { + try { + return networkInterfaceProvider.getNetworkInterfaceAddresses().stream() + .anyMatch(a -> a != null && a.equals(address)); + } catch (SocketException e) { + throw new IllegalArgumentException("Network interfaces could not be fetched."); + } + } + } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookModule.java b/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookModule.java index 3a515608a06..97f178eaaaf 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookModule.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/webhook/WebhookModule.java @@ -25,6 +25,7 @@ public class WebhookModule extends Module { @Override protected void configureModule() { add( + NetworkInterfaceProvider.class, WebhookCustomDns.class, WebhookCallerImpl.class, WebhookDeliveryStorage.class, diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/webhook/NetworkInterfaceProviderTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/webhook/NetworkInterfaceProviderTest.java new file mode 100644 index 00000000000..be213d0bab5 --- /dev/null +++ b/server/sonar-server-common/src/test/java/org/sonar/server/webhook/NetworkInterfaceProviderTest.java @@ -0,0 +1,37 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.webhook; + +import java.net.SocketException; +import java.util.List; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NetworkInterfaceProviderTest { + private NetworkInterfaceProvider underTest = new NetworkInterfaceProvider(); + + @Test + public void itGetsListOfNetworkInterfaceAddresses() throws SocketException { + assertThat(underTest.getNetworkInterfaceAddresses()) + .isInstanceOf(List.class) + .hasSizeGreaterThan(0); + } +} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCallerImplTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCallerImplTest.java index be4060c8779..885ecae43a6 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCallerImplTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCallerImplTest.java @@ -19,6 +19,8 @@ */ package org.sonar.server.webhook; +import com.google.common.collect.ImmutableList; +import java.net.InetAddress; import java.util.Optional; import okhttp3.Credentials; import okhttp3.HttpUrl; @@ -63,6 +65,7 @@ public class WebhookCallerImplTest { public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60)); Configuration configuration = Mockito.mock(Configuration.class); + NetworkInterfaceProvider networkInterfaceProvider = Mockito.mock(NetworkInterfaceProvider.class); private System2 system = new TestSystem2().setNow(NOW); @@ -248,6 +251,29 @@ public class WebhookCallerImplTest { assertThat(delivery.getPayload()).isSameAs(PAYLOAD); } + @Test + public void silently_catch_error_when_url_is_local_network_interface() throws Exception { + String url = "https://192.168.1.21"; + + InetAddress inetAddress = InetAddress.getByName(HttpUrl.parse(url).host()); + + when(networkInterfaceProvider.getNetworkInterfaceAddresses()) + .thenReturn(ImmutableList.of(inetAddress)); + + Webhook webhook = new Webhook(WEBHOOK_UUID, PROJECT_UUID, CE_TASK_UUID, + randomAlphanumeric(40), "my-webhook", url, null); + + WebhookDelivery delivery = newSender(true).call(webhook, PAYLOAD); + + assertThat(delivery.getHttpStatus()).isEmpty(); + assertThat(delivery.getDurationInMs().get()).isNotNegative(); + assertThat(delivery.getError().get()).isInstanceOf(IllegalArgumentException.class); + assertThat(delivery.getErrorMessage()).contains("Invalid URL: loopback and wildcard addresses are not allowed for webhooks."); + assertThat(delivery.getAt()).isEqualTo(NOW); + assertThat(delivery.getWebhook()).isSameAs(webhook); + assertThat(delivery.getPayload()).isSameAs(PAYLOAD); + } + private RecordedRequest takeAndVerifyPostRequest(String expectedPath) throws Exception { RecordedRequest request = server.takeRequest(); @@ -260,7 +286,7 @@ public class WebhookCallerImplTest { private WebhookCaller newSender(boolean validateWebhook) { SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.parse("6.2"), SonarQubeSide.SERVER, SonarEdition.COMMUNITY); when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(validateWebhook)); - WebhookCustomDns webhookCustomDns = new WebhookCustomDns(configuration); + WebhookCustomDns webhookCustomDns = new WebhookCustomDns(configuration, networkInterfaceProvider); return new WebhookCallerImpl(system, new OkHttpClientProvider().provide(new MapSettings().asConfig(), runtime), webhookCustomDns); } } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCustomDnsTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCustomDnsTest.java index af468b40f36..8c6906b9490 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCustomDnsTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/webhook/WebhookCustomDnsTest.java @@ -19,9 +19,12 @@ */ package org.sonar.server.webhook; +import com.google.common.collect.ImmutableList; import java.net.InetAddress; +import java.net.SocketException; import java.net.UnknownHostException; import java.util.Optional; +import okhttp3.HttpUrl; import org.assertj.core.api.Assertions; import org.junit.Test; import org.mockito.Mockito; @@ -31,16 +34,19 @@ import static org.mockito.Mockito.when; import static org.sonar.process.ProcessProperties.Property.SONAR_VALIDATE_WEBHOOKS; public class WebhookCustomDnsTest { + private static final String INVALID_URL = "Invalid URL: loopback and wildcard addresses are not allowed for webhooks."; private Configuration configuration = Mockito.mock(Configuration.class); - private WebhookCustomDns underTest = new WebhookCustomDns(configuration); + private NetworkInterfaceProvider networkInterfaceProvider = Mockito.mock(NetworkInterfaceProvider.class); + + private WebhookCustomDns underTest = new WebhookCustomDns(configuration, networkInterfaceProvider); @Test public void lookup_fail_on_localhost() { when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(true)); Assertions.assertThatThrownBy(() -> underTest.lookup("localhost")) - .hasMessageContaining("") + .hasMessageContaining(INVALID_URL) .isInstanceOf(IllegalArgumentException.class); } @@ -49,7 +55,30 @@ public class WebhookCustomDnsTest { when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(true)); Assertions.assertThatThrownBy(() -> underTest.lookup("127.0.0.1")) - .hasMessageContaining("") + .hasMessageContaining(INVALID_URL) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void lookup_fail_on_192_168_1_21() throws UnknownHostException, SocketException { + InetAddress inetAddress = InetAddress.getByName(HttpUrl.parse("https://192.168.1.21/").host()); + + when(configuration.getBoolean(SONAR_VALIDATE_WEBHOOKS.getKey())).thenReturn(Optional.of(true)); + when(networkInterfaceProvider.getNetworkInterfaceAddresses()) + .thenReturn(ImmutableList.of(inetAddress)); + + Assertions.assertThatThrownBy(() -> underTest.lookup("192.168.1.21")) + .hasMessageContaining(INVALID_URL) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void lookup_fail_on_network_interface_throwing_socket_exception() throws SocketException { + when(networkInterfaceProvider.getNetworkInterfaceAddresses()) + .thenThrow(new SocketException()); + + Assertions.assertThatThrownBy(() -> underTest.lookup("good-url.com")) + .hasMessageContaining("Network interfaces could not be fetched.") .isInstanceOf(IllegalArgumentException.class); } |