From: Belen Pruvost Date: Wed, 14 Apr 2021 13:31:51 +0000 (+0200) Subject: SONAR-14682 - Filtering local network interfaces on webhooks X-Git-Tag: 8.9.0.43852~121 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=2ac0f065bea21dda018d33e3b39ea9a4291da0ce;p=sonarqube.git SONAR-14682 - Filtering local network interfaces on webhooks --- diff --git a/server/sonar-db-migration/build.gradle b/server/sonar-db-migration/build.gradle index ebdb837087e..bdf5e2b38d1 100644 --- a/server/sonar-db-migration/build.gradle +++ b/server/sonar-db-migration/build.gradle @@ -21,6 +21,7 @@ dependencies { testCompile 'com.google.code.findbugs:jsr305' testCompile 'com.tngtech.java:junit-dataprovider' testCompile 'commons-dbutils:commons-dbutils' + testCompile 'com.squareup.okhttp3:mockwebserver' testCompile 'junit:junit' testCompile 'org.assertj:assertj-core' testCompile 'org.mindrot:jbcrypt' diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java index 1b393d2d3c1..29853fbbb5e 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java @@ -37,6 +37,7 @@ import org.sonar.server.platform.db.migration.version.v86.DbVersion86; import org.sonar.server.platform.db.migration.version.v87.DbVersion87; import org.sonar.server.platform.db.migration.version.v88.DbVersion88; import org.sonar.server.platform.db.migration.version.v89.DbVersion89; +import org.sonar.server.platform.db.migration.version.v89.util.NetworkInterfaceProvider; public class MigrationConfigurationModule extends Module { @Override @@ -65,6 +66,7 @@ public class MigrationConfigurationModule extends Module { // Utility classes DbPrimaryKeyConstraintFinder.class, - DropPrimaryKeySqlGenerator.class); + DropPrimaryKeySqlGenerator.class, + NetworkInterfaceProvider.class); } } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/DbVersion89.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/DbVersion89.java index b160344075e..0c9ff6e637a 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/DbVersion89.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/DbVersion89.java @@ -27,6 +27,7 @@ public class DbVersion89 implements DbVersion { @Override public void addSteps(MigrationStepRegistry registry) { registry - .add(4400, "Add indices on columns 'type' and 'value' to 'new_code_periods' table", AddIndicesToNewCodePeriodTable.class); + .add(4400, "Add indices on columns 'type' and 'value' to 'new_code_periods' table", AddIndicesToNewCodePeriodTable.class) + .add(4401, "Drop local webhooks", DropLocalWebhooks.class); } } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooks.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooks.java new file mode 100644 index 00000000000..41b77b1dc66 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooks.java @@ -0,0 +1,82 @@ +/* + * 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.platform.db.migration.version.v89; + +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.SocketException; +import java.net.URL; +import java.net.UnknownHostException; +import java.sql.SQLException; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.db.Database; +import org.sonar.server.platform.db.migration.step.DataChange; +import org.sonar.server.platform.db.migration.step.MassUpdate; +import org.sonar.server.platform.db.migration.version.v89.util.NetworkInterfaceProvider; + +public class DropLocalWebhooks extends DataChange { + private static final Logger LOG = Loggers.get(DropLocalWebhooks.class); + + private final NetworkInterfaceProvider networkInterfaceProvider; + + public DropLocalWebhooks(Database db, NetworkInterfaceProvider networkInterfaceProvider) { + super(db); + this.networkInterfaceProvider = networkInterfaceProvider; + } + + @Override + protected void execute(Context context) throws SQLException { + MassUpdate massUpdate = context.prepareMassUpdate(); + massUpdate.select("select w.uuid, w.name, w.url, w.project_uuid, p.name from webhooks w left join projects p on p.uuid = w.project_uuid"); + massUpdate.update("delete from webhooks where uuid = ?"); + massUpdate.execute((row, update) -> { + try { + String webhookName = row.getString(2); + String webhookUrl = row.getString(3); + URL url = new URL(webhookUrl); + InetAddress address = InetAddress.getByName(url.getHost()); + if (isLocalAddress(address)) { + boolean projectLevel = row.getString(4) != null; + if (projectLevel) { + String projectName = row.getString(5); + LOG.warn("Webhook '{}' for project '{}' has been removed because it used an invalid, unsafe URL. Please recreate " + + "this webhook with a valid URL or ask a project administrator to do it if it is still needed.", webhookName, projectName); + } else { + LOG.warn("Global webhook '{}' has been removed because it used an invalid, unsafe URL. Please recreate this webhook with a valid URL" + + " if it is still needed.", webhookName); + } + + update.setString(1, row.getString(1)); + return true; + } + } catch (MalformedURLException | UnknownHostException | SocketException e) { + return false; + } + + return false; + }); + } + + private boolean isLocalAddress(InetAddress address) throws SocketException { + return networkInterfaceProvider.getNetworkInterfaceAddresses().stream() + .anyMatch(a -> a != null && a.equals(address)); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/util/NetworkInterfaceProvider.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/util/NetworkInterfaceProvider.java new file mode 100644 index 00000000000..e3df6bd68d1 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v89/util/NetworkInterfaceProvider.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.platform.db.migration.version.v89.util; + +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; + +public class NetworkInterfaceProvider { + + public List getNetworkInterfaceAddresses() throws SocketException { + return Collections.list(NetworkInterface.getNetworkInterfaces()) + .stream() + .flatMap(ni -> Collections.list(ni.getInetAddresses()).stream()) + .collect(Collectors.toList()); + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooksTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooksTest.java new file mode 100644 index 00000000000..765f769f3eb --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooksTest.java @@ -0,0 +1,153 @@ +/* + * 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.platform.db.migration.version.v89; + +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.net.InetAddress; +import java.sql.SQLException; +import java.util.List; +import javax.annotation.Nullable; +import okhttp3.HttpUrl; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.log.LogTester; +import org.sonar.api.utils.log.LoggerLevel; +import org.sonar.db.CoreDbTester; +import org.sonar.server.platform.db.migration.step.DataChange; +import org.sonar.server.platform.db.migration.version.v89.util.NetworkInterfaceProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DropLocalWebhooksTest { + + @Rule + public LogTester logTester = new LogTester(); + + private static final String TABLE_NAME = "webhooks"; + + @Rule + public CoreDbTester dbTester = CoreDbTester.createForSchema(DropLocalWebhooksTest.class, "schema.sql"); + + private final NetworkInterfaceProvider networkInterfaceProvider = mock(NetworkInterfaceProvider.class); + + private final DataChange underTest = new DropLocalWebhooks(dbTester.database(), networkInterfaceProvider); + + @Before + public void prepare() throws IOException { + InetAddress inetAddress1 = InetAddress.getByName(HttpUrl.parse("https://0.0.0.0/some_webhook").host()); + InetAddress inetAddress2 = InetAddress.getByName(HttpUrl.parse("https://127.0.0.1/some_webhook").host()); + InetAddress inetAddress3 = InetAddress.getByName(HttpUrl.parse("https://localhost/some_webhook").host()); + + when(networkInterfaceProvider.getNetworkInterfaceAddresses()) + .thenReturn(ImmutableList.of(inetAddress1, inetAddress2, inetAddress3)); + } + + @Test + public void execute() throws SQLException { + prepareWebhooks(); + + underTest.execute(); + + verifyMigrationResult(); + } + + @Test + public void migrationIsReEntrant() throws SQLException { + prepareWebhooks(); + + underTest.execute(); + underTest.execute(); + + verifyMigrationResult(); + } + + @Test + public void migrationIsSuccessfulWhenNoWebhooksDeleted() throws SQLException { + insertProject("p1", "pn1"); + insertWebhook("uuid-1", "https://10.15.15.15:5555/some_webhook", "p1"); + insertWebhook("uuid-5", "https://some.valid.address.com/random_webhook", null); + + underTest.execute(); + + assertThat(dbTester.countRowsOfTable(TABLE_NAME)).isEqualTo(2); + assertThat(logTester.logs(LoggerLevel.WARN)).isEmpty(); + } + + @Test + public void migrationIsSuccessfulWhenNoWebhooksInDb() throws SQLException { + insertProject("p1", "pn1"); + + underTest.execute(); + + assertThat(dbTester.countRowsOfTable(TABLE_NAME)).isZero(); + assertThat(logTester.logs(LoggerLevel.WARN)).isEmpty(); + } + + private void prepareWebhooks() { + insertProject("p1", "pn1"); + insertProject("p2", "pn2"); + insertWebhook("uuid-1", "https://10.15.15.15:5555/some_webhook", "p1"); + insertWebhook("uuid-2", "https://0.0.0.0/some_webhook", "p1"); + insertWebhook("uuid-3", "https://172.16.16.16:6666/some_webhook", "p2"); + insertWebhook("uuid-4", "https://127.0.0.1/some_webhook", "p2"); + insertWebhook("uuid-5", "https://some.valid.address.com/random_webhook", null); + insertWebhook("uuid-6", "https://248.235.76.254:7777/some_webhook", null); + insertWebhook("uuid-7", "https://localhost/some_webhook", null); + } + + private void verifyMigrationResult() { + assertThat(dbTester.countRowsOfTable(TABLE_NAME)).isEqualTo(4); + assertThat(dbTester.select("select uuid from " + TABLE_NAME).stream().map(columns -> columns.get("UUID"))) + .containsOnly("uuid-1", "uuid-3", "uuid-5", "uuid-6"); + + List logs = logTester.logs(LoggerLevel.WARN); + assertThat(logs).hasSize(3); + assertThat(logs).containsExactlyInAnyOrder( + "Global webhook 'webhook-uuid-7' has been removed because it used an invalid, unsafe URL. Please recreate this webhook with a valid URL if it is still needed.", + "Webhook 'webhook-uuid-4' for project 'pn2' has been removed because it used an invalid, unsafe URL. Please recreate this webhook with a valid URL or ask a project administrator to do it if it is still needed.", + "Webhook 'webhook-uuid-2' for project 'pn1' has been removed because it used an invalid, unsafe URL. Please recreate this webhook with a valid URL or ask a project administrator to do it if it is still needed."); + } + + private void insertProject(String uuid, String name) { + dbTester.executeInsert("PROJECTS", + "NAME", name, + "ORGANIZATION_UUID", "default", + "KEE", uuid + "-key", + "UUID", uuid, + "PRIVATE", Boolean.toString(false), + "QUALIFIER", "TRK", + "UPDATED_AT", System2.INSTANCE.now()); + } + + private void insertWebhook(String uuid, String url, @Nullable String projectUuid) { + dbTester.executeInsert(TABLE_NAME, + "UUID", uuid, + "NAME", "webhook-" + uuid, + "PROJECT_UUID", projectUuid, + "URL", url, + "CREATED_AT", System2.INSTANCE.now()); + } +} + diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v89/util/NetworkInterfaceProviderTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v89/util/NetworkInterfaceProviderTest.java new file mode 100644 index 00000000000..47274ed5545 --- /dev/null +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v89/util/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.platform.db.migration.version.v89.util; + +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-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooksTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooksTest/schema.sql new file mode 100644 index 00000000000..de11e9e16ed --- /dev/null +++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v89/DropLocalWebhooksTest/schema.sql @@ -0,0 +1,29 @@ +CREATE TABLE "PROJECTS"( + "UUID" VARCHAR(40) NOT NULL, + "KEE" VARCHAR(400) NOT NULL, + "QUALIFIER" VARCHAR(10) NOT NULL, + "ORGANIZATION_UUID" VARCHAR(40) NOT NULL, + "NAME" VARCHAR(2000), + "DESCRIPTION" VARCHAR(2000), + "PRIVATE" BOOLEAN NOT NULL, + "TAGS" VARCHAR(500), + "CREATED_AT" BIGINT, + "UPDATED_AT" BIGINT NOT NULL +); +ALTER TABLE "PROJECTS" ADD CONSTRAINT "PK_NEW_PROJECTS" PRIMARY KEY("UUID"); +CREATE UNIQUE INDEX "UNIQ_PROJECTS_KEE" ON "PROJECTS"("KEE"); +CREATE INDEX "IDX_QUALIFIER" ON "PROJECTS"("QUALIFIER"); + +CREATE TABLE "WEBHOOKS"( + "UUID" VARCHAR(40) NOT NULL, + "ORGANIZATION_UUID" VARCHAR(40), + "PROJECT_UUID" VARCHAR(40), + "NAME" VARCHAR(100) NOT NULL, + "URL" VARCHAR(2000) NOT NULL, + "SECRET" VARCHAR(200), + "CREATED_AT" BIGINT NOT NULL, + "UPDATED_AT" BIGINT +); +ALTER TABLE "WEBHOOKS" ADD CONSTRAINT "PK_WEBHOOKS" PRIMARY KEY("UUID"); +CREATE INDEX "ORGANIZATION_WEBHOOK" ON "WEBHOOKS"("ORGANIZATION_UUID"); +CREATE INDEX "PROJECT_WEBHOOK" ON "WEBHOOKS"("PROJECT_UUID"); 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 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 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); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/NetworkInterfaceProvider.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/NetworkInterfaceProvider.java new file mode 100644 index 00000000000..dfdd636d6df --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/NetworkInterfaceProvider.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.ws; + +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; + +public class NetworkInterfaceProvider { + + public List getNetworkInterfaceAddresses() throws SocketException { + return Collections.list(NetworkInterface.getNetworkInterfaces()) + .stream() + .flatMap(ni -> Collections.list(ni.getInetAddresses()).stream()) + .collect(Collectors.toList()); + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhookSupport.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhookSupport.java index 0b94d916723..16a1b382853 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhookSupport.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhookSupport.java @@ -20,6 +20,7 @@ package org.sonar.server.webhook.ws; import java.net.InetAddress; +import java.net.SocketException; import java.net.UnknownHostException; import okhttp3.HttpUrl; import org.sonar.api.config.Configuration; @@ -34,10 +35,12 @@ public class WebhookSupport { private final UserSession userSession; private final Configuration configuration; + private final NetworkInterfaceProvider networkInterfaceProvider; - public WebhookSupport(UserSession userSession, Configuration configuration) { + public WebhookSupport(UserSession userSession, Configuration configuration, NetworkInterfaceProvider networkInterfaceProvider) { this.userSession = userSession; this.configuration = configuration; + this.networkInterfaceProvider = networkInterfaceProvider; } void checkPermission(ProjectDto projectDto) { @@ -55,13 +58,21 @@ public class WebhookSupport { throw new IllegalArgumentException(String.format(message, messageArguments)); } InetAddress address = InetAddress.getByName(okUrl.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."); } } catch (UnknownHostException e) { // if a host can not be resolved the deliveries will fail - no need to block it from being set // this will only happen for public URLs + } catch (SocketException e) { + throw new IllegalStateException("Can not retrieve a network interfaces", e); } } + + private boolean isLocalAddress(InetAddress address) throws SocketException { + return networkInterfaceProvider.getNetworkInterfaceAddresses().stream() + .anyMatch(a -> a != null && a.equals(address)); + } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java index a171e44aa8d..1adea825a11 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/webhook/ws/WebhooksWsModule.java @@ -32,6 +32,7 @@ public class WebhooksWsModule extends Module { UpdateAction.class, DeleteAction.class, WebhookDeliveryAction.class, - WebhookDeliveriesAction.class); + WebhookDeliveriesAction.class, + NetworkInterfaceProvider.class); } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/CreateActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/CreateActionTest.java index 966e6c2c6db..24153d8285f 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/CreateActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/CreateActionTest.java @@ -68,7 +68,8 @@ public class CreateActionTest { private final ComponentDbTester componentDbTester = db.components(); private final UuidFactory uuidFactory = UuidFactoryFast.getInstance(); private final Configuration configuration = mock(Configuration.class); - private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration); + private final NetworkInterfaceProvider networkInterfaceProvider = mock(NetworkInterfaceProvider.class); + private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration, networkInterfaceProvider); private final ResourceTypes resourceTypes = mock(ResourceTypes.class); private final ComponentFinder componentFinder = new ComponentFinder(dbClient, resourceTypes); private final CreateAction underTest = new CreateAction(dbClient, userSession, uuidFactory, webhookSupport, componentFinder); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/DeleteActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/DeleteActionTest.java index 1e779ff1098..0b089005b92 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/DeleteActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/DeleteActionTest.java @@ -67,7 +67,8 @@ public class DeleteActionTest { private final WebhookDeliveryDao deliveryDao = dbClient.webhookDeliveryDao(); private final ComponentDbTester componentDbTester = db.components(); private final Configuration configuration = mock(Configuration.class); - private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration); + private final NetworkInterfaceProvider networkInterfaceProvider = mock(NetworkInterfaceProvider.class); + private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration, networkInterfaceProvider); private final DeleteAction underTest = new DeleteAction(dbClient, userSession, webhookSupport); private final WsActionTester wsActionTester = new WsActionTester(underTest); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/ListActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/ListActionTest.java index bc1115c6995..0ca1bd2984c 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/ListActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/ListActionTest.java @@ -69,7 +69,8 @@ public class ListActionTest { private final DbClient dbClient = db.getDbClient(); private final Configuration configuration = mock(Configuration.class); - private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration); + private final NetworkInterfaceProvider networkInterfaceProvider = mock(NetworkInterfaceProvider.class); + private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration, networkInterfaceProvider); private final ResourceTypes resourceTypes = mock(ResourceTypes.class); private final ComponentFinder componentFinder = new ComponentFinder(dbClient, resourceTypes); private final ListAction underTest = new ListAction(dbClient, userSession, webhookSupport, componentFinder); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/NetworkInterfaceProviderTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/NetworkInterfaceProviderTest.java new file mode 100644 index 00000000000..c9b7ba62086 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/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.ws; + +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-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/UpdateActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/UpdateActionTest.java index cbc26e7c90c..0fb45101aad 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/UpdateActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/UpdateActionTest.java @@ -63,7 +63,8 @@ public class UpdateActionTest { private final WebhookDbTester webhookDbTester = db.webhooks(); private final ComponentDbTester componentDbTester = db.components(); private final Configuration configuration = mock(Configuration.class); - private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration); + private final NetworkInterfaceProvider networkInterfaceProvider = mock(NetworkInterfaceProvider.class); + private final WebhookSupport webhookSupport = new WebhookSupport(userSession, configuration, networkInterfaceProvider); private final ResourceTypes resourceTypes = mock(ResourceTypes.class); private final ComponentFinder componentFinder = new ComponentFinder(dbClient, resourceTypes); private final UpdateAction underTest = new UpdateAction(dbClient, userSession, webhookSupport, componentFinder); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/WebhookSupportTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/WebhookSupportTest.java index 2a5998691ab..2ebc93aa839 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/WebhookSupportTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/webhook/ws/WebhookSupportTest.java @@ -19,15 +19,23 @@ */ package org.sonar.server.webhook.ws; +import com.google.common.collect.ImmutableList; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.io.IOException; +import java.net.InetAddress; +import java.net.SocketException; +import okhttp3.HttpUrl; +import org.assertj.core.api.Assertions; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.sonar.api.config.Configuration; import org.sonar.server.user.UserSession; import static java.util.Optional.of; +import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; @@ -36,7 +44,8 @@ import static org.mockito.Mockito.when; @RunWith(DataProviderRunner.class) public class WebhookSupportTest { private final Configuration configuration = mock(Configuration.class); - private final WebhookSupport underTest = new WebhookSupport(mock(UserSession.class), configuration); + private final NetworkInterfaceProvider networkInterfaceProvider = mock(NetworkInterfaceProvider.class); + private final WebhookSupport underTest = new WebhookSupport(mock(UserSession.class), configuration, networkInterfaceProvider); @DataProvider public static Object[][] validUrls() { @@ -64,9 +73,19 @@ public class WebhookSupportTest { {"https://127.0.0.1:7777/some_webhook"}, {"https://localhost/some_webhook"}, {"https://localhost:9999/some_webhook"}, + {"https://192.168.1.21/"}, }; } + @Before + public void prepare() throws IOException { + InetAddress inetAddress = InetAddress.getByName(HttpUrl.parse("https://192.168.1.21/").host()); + + when(networkInterfaceProvider.getNetworkInterfaceAddresses()) + .thenReturn(ImmutableList.of(inetAddress)); + } + + @Test @UseDataProvider("validUrls") public void checkUrlPatternSuccessfulForValidAddress(String url) { @@ -88,4 +107,13 @@ public class WebhookSupportTest { assertThatCode(() -> underTest.checkUrlPattern(url, "msg")).doesNotThrowAnyException(); } + + @Test + public void itThrowsIllegalExceptionIfGettingNetworkInterfaceAddressesFails() throws SocketException { + when(networkInterfaceProvider.getNetworkInterfaceAddresses()).thenThrow(new SocketException()); + + assertThatThrownBy(() -> underTest.checkUrlPattern("good-url.com", "msg")) + .hasMessageContaining("") + .isInstanceOf(IllegalArgumentException.class); + } } diff --git a/server/sonar-webserver-webapi/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/server/sonar-webserver-webapi/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000000..1f0955d450f --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline