From e11a522a71ccf557254aa11ce7e0f15550f4b263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Han=C3=A1k?= Date: Thu, 13 Jul 2023 14:32:33 +0200 Subject: [PATCH] RHPAM-4719: Persistent Cross-Site Scripting (XSS) (#1393) * RHPAM-4719: Persistent Cross-Site Scripting (XSS) Fixes RHPAM-4716 & RHPAM-4717 by using StringEscapeUtils:: escapeHtml4() method in ProjectResource and by implementing helper method, using escapeHtml4(), to escape conrtributors names in OrganizationalUnitServiceImpl * RHPAM-4719: Add unit test cases for XSS data * RHPAM-4719: Replace single qoute with nothing * RHPAM-4917: Expand escaping to RepositoryService Refactors unit tests to use same methods as in main classes Add some unit tests * Fix code duplication Moves methods for escaping out of services * Increase coverage and remove code smells --- uberfire-rest/uberfire-rest-backend/pom.xml | 5 + .../guvnor/rest/backend/ProjectResource.java | 17 ++- .../rest/backend/ProjectResourceJobTest.java | 35 ++++++ .../uberfire-structure-backend/pom.xml | 5 + .../structure/backend/InputEscapeUtils.java | 53 +++++++++ .../OrganizationalUnitServiceImpl.java | 6 +- .../repositories/RepositoryServiceImpl.java | 3 +- .../backend/InputEscapeUtilsTest.java | 33 ++++++ .../OrganizationalUnitServiceTest.java | 107 ++++++++++++++++++ .../RepositoryServiceImplTest.java | 48 ++++++++ 10 files changed, 306 insertions(+), 6 deletions(-) create mode 100644 uberfire-structure/uberfire-structure-backend/src/main/java/org/guvnor/structure/backend/InputEscapeUtils.java create mode 100644 uberfire-structure/uberfire-structure-backend/src/test/java/org/guvnor/structure/backend/InputEscapeUtilsTest.java diff --git a/uberfire-rest/uberfire-rest-backend/pom.xml b/uberfire-rest/uberfire-rest-backend/pom.xml index 272a123493..5021094d9d 100644 --- a/uberfire-rest/uberfire-rest-backend/pom.xml +++ b/uberfire-rest/uberfire-rest-backend/pom.xml @@ -72,6 +72,11 @@ slf4j-api + + org.apache.commons + commons-text + + jakarta.annotation diff --git a/uberfire-rest/uberfire-rest-backend/src/main/java/org/guvnor/rest/backend/ProjectResource.java b/uberfire-rest/uberfire-rest-backend/src/main/java/org/guvnor/rest/backend/ProjectResource.java index f563bc8f2d..e7ace11262 100644 --- a/uberfire-rest/uberfire-rest-backend/src/main/java/org/guvnor/rest/backend/ProjectResource.java +++ b/uberfire-rest/uberfire-rest-backend/src/main/java/org/guvnor/rest/backend/ProjectResource.java @@ -46,6 +46,7 @@ import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.Variant; +import org.apache.commons.text.StringEscapeUtils; import org.guvnor.common.services.project.model.WorkspaceProject; import org.guvnor.common.services.project.service.WorkspaceProjectService; import org.guvnor.rest.client.AddBranchJobRequest; @@ -373,7 +374,7 @@ public Response addBranch(@PathParam("spaceName") String spaceName, jobRequest.setJobId(id); jobRequest.setSpaceName(spaceName); jobRequest.setProjectName(projectName); - jobRequest.setNewBranchName(addBranchRequest.getNewBranchName()); + jobRequest.setNewBranchName(escapeHtmlInput(addBranchRequest.getNewBranchName())); jobRequest.setBaseBranchName(addBranchRequest.getBaseBranchName()); jobRequest.setUserIdentifier(sessionInfo.getIdentity().getIdentifier()); addAcceptedJobResult(id); @@ -454,6 +455,16 @@ private ProjectResponse getProjectResponse(WorkspaceProject workspaceProject) { return projectResponse; } + private String escapeHtmlInput(String input) { + if (input != null) { + String escapedInput = StringEscapeUtils.escapeHtml4(input); + escapedInput = escapedInput.replace("'", ""); + return escapedInput; + } else { + return null; + } + } + @POST @Produces(MediaType.APPLICATION_JSON) @Path("/spaces/{spaceName}/projects/{projectName}/maven/compile") @@ -684,7 +695,7 @@ public Response createSpace(Space space) { jobRequest.setJobId(id); jobRequest.setSpaceName(space.getName()); jobRequest.setDescription(space.getDescription()); - jobRequest.setOwner(space.getOwner()); + jobRequest.setOwner(escapeHtmlInput(space.getOwner())); jobRequest.setDefaultGroupId(space.getDefaultGroupId()); addAcceptedJobResult(id); @@ -709,7 +720,7 @@ public Response updateSpace(Space space) { jobRequest.setJobId(id); jobRequest.setSpaceName(space.getName()); jobRequest.setDescription(space.getDescription()); - jobRequest.setOwner(space.getOwner()); + jobRequest.setOwner(escapeHtmlInput(space.getOwner())); jobRequest.setDefaultGroupId(space.getDefaultGroupId()); addAcceptedJobResult(id); diff --git a/uberfire-rest/uberfire-rest-backend/src/test/java/org/guvnor/rest/backend/ProjectResourceJobTest.java b/uberfire-rest/uberfire-rest-backend/src/test/java/org/guvnor/rest/backend/ProjectResourceJobTest.java index aed0157335..3b4f768b57 100644 --- a/uberfire-rest/uberfire-rest-backend/src/test/java/org/guvnor/rest/backend/ProjectResourceJobTest.java +++ b/uberfire-rest/uberfire-rest-backend/src/test/java/org/guvnor/rest/backend/ProjectResourceJobTest.java @@ -315,6 +315,28 @@ public void updateSpace() throws Exception { assertEquals(JobStatus.ACCEPTED, jobResultArgumentCaptor.getValue().getStatus()); } + @Test + public void updateSpaceWithXSSOwer() throws Exception { + String xssOwner = ""; + Space testedSpace = new Space(); + testedSpace.setOwner(xssOwner); + projectResource.updateSpace(testedSpace); + + verify(jobManager).putJob(jobResultArgumentCaptor.capture()); + assertEquals(JobStatus.ACCEPTED, jobResultArgumentCaptor.getValue().getStatus()); + } + + @Test + public void createSpaceWithXSSOwner() throws Exception { + String xssOwner = ""; + Space testedSpace = new Space(); + testedSpace.setOwner(xssOwner); + projectResource.createSpace(testedSpace); + + verify(jobManager).putJob(jobResultArgumentCaptor.capture()); + assertEquals(JobStatus.ACCEPTED, jobResultArgumentCaptor.getValue().getStatus()); + } + @Test public void deleteSpace() throws Exception { @@ -334,6 +356,19 @@ public void addBranch() { assertEquals(JobStatus.ACCEPTED, jobResultArgumentCaptor.getValue().getStatus()); } + @Test + public void addBranchWithXSSName() { + AddBranchRequest addBranchRequest = new AddBranchRequest(); + addBranchRequest.setNewBranchName(""); + + projectResource.addBranch("spaceName", + "projectName", + addBranchRequest); + + verify(jobManager).putJob(jobResultArgumentCaptor.capture()); + assertEquals(JobStatus.ACCEPTED, jobResultArgumentCaptor.getValue().getStatus()); + } + @Test public void removeBranch() { projectResource.removeBranch("spaceName", diff --git a/uberfire-structure/uberfire-structure-backend/pom.xml b/uberfire-structure/uberfire-structure-backend/pom.xml index 568e4754e0..c849864127 100644 --- a/uberfire-structure/uberfire-structure-backend/pom.xml +++ b/uberfire-structure/uberfire-structure-backend/pom.xml @@ -140,6 +140,11 @@ commons-fileupload + + org.apache.commons + commons-text + + org.uberfire uberfire-security-management-api diff --git a/uberfire-structure/uberfire-structure-backend/src/main/java/org/guvnor/structure/backend/InputEscapeUtils.java b/uberfire-structure/uberfire-structure-backend/src/main/java/org/guvnor/structure/backend/InputEscapeUtils.java new file mode 100644 index 0000000000..2a441a2090 --- /dev/null +++ b/uberfire-structure/uberfire-structure-backend/src/main/java/org/guvnor/structure/backend/InputEscapeUtils.java @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.guvnor.structure.backend; + +import java.util.ArrayList; +import java.util.Collection; + +import org.apache.commons.text.StringEscapeUtils; +import org.uberfire.security.Contributor; + +/** + * Utility class to provide input escaping or other + * functionality needed by backend services to sanitize inputs. + */ +public class InputEscapeUtils { + + private InputEscapeUtils() { + // non-instantiable class + } + + public static Collection escapeContributorsNames(Collection contributors) { + Collection escapedContributors = new ArrayList<>(); + contributors.forEach((contributor -> { + String escapedName = escapeHtmlInput(contributor.getUsername()); + escapedContributors.add(new Contributor(escapedName, contributor.getType())); + })); + return escapedContributors; + } + + public static String escapeHtmlInput(String input) { + if (input != null) { + String escapedInput = StringEscapeUtils.escapeHtml4(input); + escapedInput = escapedInput.replace("'", ""); + return escapedInput; + } else { + return null; + } + } + +} diff --git a/uberfire-structure/uberfire-structure-backend/src/main/java/org/guvnor/structure/backend/organizationalunit/OrganizationalUnitServiceImpl.java b/uberfire-structure/uberfire-structure-backend/src/main/java/org/guvnor/structure/backend/organizationalunit/OrganizationalUnitServiceImpl.java index f106eec3bd..439b1d26fd 100644 --- a/uberfire-structure/uberfire-structure-backend/src/main/java/org/guvnor/structure/backend/organizationalunit/OrganizationalUnitServiceImpl.java +++ b/uberfire-structure/uberfire-structure-backend/src/main/java/org/guvnor/structure/backend/organizationalunit/OrganizationalUnitServiceImpl.java @@ -64,6 +64,8 @@ import org.uberfire.spaces.Space; import org.uberfire.spaces.SpacesAPI; +import static org.guvnor.structure.backend.InputEscapeUtils.escapeContributorsNames; + @Service @ApplicationScoped public class OrganizationalUnitServiceImpl implements OrganizationalUnitService { @@ -320,7 +322,7 @@ public OrganizationalUnit createOrganizationalUnit(final String name, final SpaceInfo spaceInfo = new SpaceInfo(name, description, _defaultGroupId, - contributors, + escapeContributorsNames(contributors), getRepositoryAliases(repositories), Collections.emptyList()); spaceConfigStorageRegistry.get(name).saveSpaceInfo(spaceInfo); @@ -378,7 +380,7 @@ public OrganizationalUnit updateOrganizationalUnit(String name, spaceInfo.setDefaultGroupId(_defaultGroupId); if (contributors != null) { - spaceInfo.setContributors(contributors); + spaceInfo.setContributors(escapeContributorsNames(contributors)); } if (description != null) { diff --git a/uberfire-structure/uberfire-structure-backend/src/main/java/org/guvnor/structure/backend/repositories/RepositoryServiceImpl.java b/uberfire-structure/uberfire-structure-backend/src/main/java/org/guvnor/structure/backend/repositories/RepositoryServiceImpl.java index 9657010df3..ae2c5c506f 100644 --- a/uberfire-structure/uberfire-structure-backend/src/main/java/org/guvnor/structure/backend/repositories/RepositoryServiceImpl.java +++ b/uberfire-structure/uberfire-structure-backend/src/main/java/org/guvnor/structure/backend/repositories/RepositoryServiceImpl.java @@ -67,6 +67,7 @@ import org.uberfire.spaces.Space; import org.uberfire.spaces.SpacesAPI; +import static org.guvnor.structure.backend.InputEscapeUtils.escapeContributorsNames; import static org.guvnor.structure.repositories.EnvironmentParameters.CRYPT_PREFIX; import static org.guvnor.structure.repositories.EnvironmentParameters.SECURE_PREFIX; import static org.guvnor.structure.repositories.EnvironmentParameters.SCHEME; @@ -539,7 +540,7 @@ public void updateContributors(final Repository repository, thisRepositoryConfig.ifPresent(config -> { config.getConfiguration().add("contributors", - contributors); + escapeContributorsNames(contributors)); this.saveRepositoryConfig(repository.getSpace().getName(), config); repositoryContributorsUpdatedEvent.fire(new RepositoryContributorsUpdatedEvent(getRepositoryFromSpace(repository.getSpace(), diff --git a/uberfire-structure/uberfire-structure-backend/src/test/java/org/guvnor/structure/backend/InputEscapeUtilsTest.java b/uberfire-structure/uberfire-structure-backend/src/test/java/org/guvnor/structure/backend/InputEscapeUtilsTest.java new file mode 100644 index 0000000000..5f428c8878 --- /dev/null +++ b/uberfire-structure/uberfire-structure-backend/src/test/java/org/guvnor/structure/backend/InputEscapeUtilsTest.java @@ -0,0 +1,33 @@ +package org.guvnor.structure.backend; + +import org.apache.commons.text.StringEscapeUtils; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import static org.guvnor.structure.backend.InputEscapeUtils.escapeHtmlInput; + +public class InputEscapeUtilsTest { + + @Test + public void testEscapeHtmlInput() { + final String xssString = ""; + final String expectedResult = StringEscapeUtils.escapeHtml4(xssString).replace("'", ""); + final String result = escapeHtmlInput(xssString); + Assertions.assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void testEscapeHtmlInputWithSingleQoutes() { + final String xssString = ""; + final String expectedResult = StringEscapeUtils.escapeHtml4(xssString).replace("'", ""); + final String result = escapeHtmlInput(xssString); + Assertions.assertThat(result).isEqualTo(expectedResult); + } + + @Test + public void testEscapeHtmlInputNull() { + final String xssString = null; + final String result = escapeHtmlInput(xssString); + Assertions.assertThat(result).isNull(); + } +} diff --git a/uberfire-structure/uberfire-structure-backend/src/test/java/org/guvnor/structure/backend/organizationalunit/OrganizationalUnitServiceTest.java b/uberfire-structure/uberfire-structure-backend/src/test/java/org/guvnor/structure/backend/organizationalunit/OrganizationalUnitServiceTest.java index ad4f813252..54fd6bda48 100644 --- a/uberfire-structure/uberfire-structure-backend/src/test/java/org/guvnor/structure/backend/organizationalunit/OrganizationalUnitServiceTest.java +++ b/uberfire-structure/uberfire-structure-backend/src/test/java/org/guvnor/structure/backend/organizationalunit/OrganizationalUnitServiceTest.java @@ -63,6 +63,7 @@ import org.uberfire.spaces.Space; import org.uberfire.spaces.SpacesAPI; +import static org.guvnor.structure.backend.InputEscapeUtils.escapeHtmlInput; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -268,6 +269,78 @@ public void createValidOrganizationalUnitTest() { assertEquals(SPACE_DESCRIPTION, ou.getDescription()); assertEquals(DEFAULT_GROUP_ID, ou.getDefaultGroupId()); assertEquals(contributors, ou.getContributors()); + Assertions.assertThat(ou.getContributors()).hasSize(1); + Assertions.assertThat(ou.getContributors()).hasOnlyOneElementSatisfying((contributor) -> { + contributor.getUsername().equals(escapeHtmlInput(ADMIN)); + }); + } + + @Test + public void createOrganizationalUnitWithPersistentXssInContributorTest() { + final String persistentXssContributor = ""; + final String escapedPersistentXssContributor = escapeHtmlInput(persistentXssContributor); + + List contributors = new ArrayList<>(); + contributors.add(new Contributor(persistentXssContributor, + ContributorType.ADMIN)); + + setOUCreationPermission(true); + + final OrganizationalUnit ou = organizationalUnitService.createOrganizationalUnit(SPACE_NAME, + DEFAULT_GROUP_ID, + new ArrayList<>(), + contributors, + SPACE_DESCRIPTION); + + assertNotNull(ou); + verify(organizationalUnitFactory).newOrganizationalUnit(any()); + assertEquals(SPACE_NAME, ou.getName()); + assertEquals(SPACE_DESCRIPTION, ou.getDescription()); + assertEquals(DEFAULT_GROUP_ID, ou.getDefaultGroupId()); + + Assertions.assertThat(ou.getContributors()).hasSize(1); + Assertions.assertThat(ou.getContributors()).hasOnlyOneElementSatisfying((contributor) -> { + contributor.getUsername().equals(escapedPersistentXssContributor); + }); + } + + @Test + public void createOrganizationalUnitWithPersistentXssAndValidContributorTest() { + final String persistentXssContributor = ""; + final String escapedPersistentXssContributor = escapeHtmlInput(persistentXssContributor); + final String escapedAdminContributor = escapeHtmlInput(ADMIN); + final String regularContributor = "head_technician_junior-intern"; + final String escapedRegularContributor = escapeHtmlInput(regularContributor); + + List contributors = new ArrayList<>(); + contributors.add(new Contributor(persistentXssContributor, + ContributorType.CONTRIBUTOR)); + contributors.add(new Contributor(ADMIN, + ContributorType.ADMIN)); + contributors.add(new Contributor(regularContributor, + ContributorType.OWNER)); + + setOUCreationPermission(true); + + final OrganizationalUnit ou = organizationalUnitService.createOrganizationalUnit(SPACE_NAME, + DEFAULT_GROUP_ID, + new ArrayList<>(), + contributors, + SPACE_DESCRIPTION); + + assertNotNull(ou); + verify(organizationalUnitFactory).newOrganizationalUnit(any()); + assertEquals(SPACE_NAME, ou.getName()); + assertEquals(SPACE_DESCRIPTION, ou.getDescription()); + assertEquals(DEFAULT_GROUP_ID, ou.getDefaultGroupId()); + + Assertions.assertThat(ou.getContributors()).hasSize(3); + Assertions.assertThat(ou.getContributors()).containsExactly(new Contributor(escapedPersistentXssContributor, + ContributorType.CONTRIBUTOR), + new Contributor(escapedAdminContributor, + ContributorType.ADMIN), + new Contributor(escapedRegularContributor, + ContributorType.OWNER)); } @Test @@ -326,6 +399,40 @@ public void testUpdateOrganizationalUnit() { verify(spaceConfigStorage).endBatch(); } + @Test + public void testContributorsPersistentXssOnUpdateOrganizationalUnit() { + final String persistentXssContributor = ""; + final String escapedPersistentXssContributor = escapeHtmlInput(persistentXssContributor); + + OrganizationalUnit organizationalUnit = + organizationalUnitService.updateOrganizationalUnit(SPACE_NAME, + DEFAULT_GROUP_ID, + Collections.singletonList( + new Contributor( + persistentXssContributor, + ContributorType.ADMIN + ) + ) + ); + + Assertions.assertThat(organizationalUnit) + .hasFieldOrPropertyWithValue("name", SPACE_NAME) + .hasFieldOrPropertyWithValue("defaultGroupId", DEFAULT_GROUP_ID); + + Assertions.assertThat(organizationalUnit.getContributors()).hasSize(1); + Assertions.assertThat(organizationalUnit.getContributors()).hasOnlyOneElementSatisfying((contributor) -> { + contributor.getUsername().equals(escapedPersistentXssContributor); + }); + + Assertions.assertThat(spaceInfo) + .hasFieldOrPropertyWithValue("name", SPACE_NAME) + .hasFieldOrPropertyWithValue("defaultGroupId", DEFAULT_GROUP_ID); + + verify(spaceConfigStorage).startBatch(); + verify(spaceConfigStorage).saveSpaceInfo(eq(spaceInfo)); + verify(spaceConfigStorage).endBatch(); + } + @Test public void testCheckChildrenRepositoryContributors() { OrganizationalUnit organizationalUnit = new OrganizationalUnitImpl(SPACE_NAME, DEFAULT_GROUP_ID); diff --git a/uberfire-structure/uberfire-structure-backend/src/test/java/org/guvnor/structure/backend/repositories/RepositoryServiceImplTest.java b/uberfire-structure/uberfire-structure-backend/src/test/java/org/guvnor/structure/backend/repositories/RepositoryServiceImplTest.java index 3eef397a94..b85e4fa331 100644 --- a/uberfire-structure/uberfire-structure-backend/src/test/java/org/guvnor/structure/backend/repositories/RepositoryServiceImplTest.java +++ b/uberfire-structure/uberfire-structure-backend/src/test/java/org/guvnor/structure/backend/repositories/RepositoryServiceImplTest.java @@ -39,6 +39,7 @@ import org.uberfire.spaces.Space; import org.uberfire.spaces.SpacesAPI; +import static org.guvnor.structure.backend.InputEscapeUtils.escapeHtmlInput; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -171,6 +172,53 @@ public void updateContributorsTest() { verify(spaceConfigStorage).endBatch(); } + @Test + public void updateContributorsWithXSSNameTest() { + + final Space space = new Space("space"); + doReturn(space).when(repository).getSpace(); + doReturn("alias").when(repository).getAlias(); + + doReturn(repository).when(configuredRepositories).getRepositoryByRepositoryAlias(any(), + any()); + + final SpaceConfigStorage spaceConfigStorage = mock(SpaceConfigStorage.class); + doReturn(new SpaceInfo("space", + "Test space", + "defaultGroupId", + Collections.emptyList(), + new ArrayList<>(Arrays.asList(new RepositoryInfo("alias", + false, + new RepositoryConfiguration()))), + Collections.emptyList())).when(spaceConfigStorage).loadSpaceInfo(); + + when(registry.get(anyString())).thenReturn(spaceConfigStorage); + when(registry.getBatch(anyString())).thenReturn(new SpaceConfigStorageRegistryImpl.SpaceStorageBatchImpl(spaceConfigStorage)); + + final String xssName = ""; + final String escapedXssName = escapeHtmlInput(xssName); + repositoryService.updateContributors(repository, + Collections.singletonList(new Contributor(xssName, + ContributorType.OWNER))); + + verify(updatedEvent).fire(captor.capture()); + assertEquals("alias", + captor.getValue().getRepository().getAlias()); + assertEquals("space", + captor.getValue().getRepository().getSpace().getName()); + verify(repositoryService).saveRepositoryConfig(eq("space"), + configCaptor.capture()); + + assertEquals(escapedXssName, + configCaptor.getValue().getContributors().get(0).getUsername()); + assertEquals(ContributorType.OWNER, + configCaptor.getValue().getContributors().get(0).getType()); + + verify(spaceConfigStorage).startBatch(); + verify(spaceConfigStorage).saveSpaceInfo(any()); + verify(spaceConfigStorage).endBatch(); + } + @Test public void testDoRemoveInOrder() {