Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2e1efc0
adds sanitizer
RuthKirby Mar 18, 2026
9e1fef9
wip
RuthKirby Mar 18, 2026
7af38de
Draft sanitizer via annotation
a-sealey-justice-gov Mar 20, 2026
304a077
Merge remote-tracking branch 'origin/S28-2903-data-validation' into S…
a-sealey-justice-gov Mar 20, 2026
0e8d171
sanitization annotation
RuthKirby Mar 20, 2026
215e888
Merge remote-tracking branch 'origin/S28-2903-data-validation' into S…
RuthKirby Mar 20, 2026
9d744dd
delete filter
RuthKirby Mar 20, 2026
c25fa62
checkstyle fixes
RuthKirby Mar 23, 2026
075045a
wip CreateAuditDTO
RuthKirby Mar 23, 2026
1c3e364
adds validation to more DTO fields and controller param
RuthKirby Mar 23, 2026
23b379e
adds sanitized annotation
RuthKirby Mar 23, 2026
001978c
cleanup
RuthKirby Mar 23, 2026
73c700c
add tests
RuthKirby Mar 23, 2026
6a4a0eb
add tests
RuthKirby Mar 23, 2026
72f79f1
adds/removes sanitized annotation
RuthKirby Mar 23, 2026
a5ed242
wip functional tests
RuthKirby Mar 24, 2026
b12d14a
csv for validation test
RuthKirby Mar 24, 2026
33644b5
JSON validator
a-sealey-justice-gov Mar 24, 2026
a3e5640
JSON validator tests
a-sealey-justice-gov Mar 24, 2026
f63ac48
tests and cleanup
RuthKirby Mar 24, 2026
63dd3bb
removes unneeded annotation
RuthKirby Mar 24, 2026
16f503c
checkstyle fixes
RuthKirby Mar 24, 2026
0aa75e2
pmd fixes
RuthKirby Mar 24, 2026
1856372
removes JSON validator to be completed in other ticket
RuthKirby Mar 25, 2026
a3d70a8
removes JSON validator to be completed in other ticket
RuthKirby Mar 25, 2026
26fab65
cleanup
RuthKirby Mar 25, 2026
9668ab1
cleanup
RuthKirby Mar 25, 2026
2359e66
checkstyle fix
RuthKirby Mar 25, 2026
8610b8d
remove audit details check to be done in future ticket
RuthKirby Mar 25, 2026
a4d6d7c
checkstyle fix
RuthKirby Mar 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@ dependencies {
implementation group: 'com.microsoft.graph', name: 'microsoft-graph', version: '6.24.0'
implementation group: 'com.azure', name: 'azure-identity', version: '1.18.2'

implementation group: 'org.jsoup', name: 'jsoup', version: '1.22.1'

developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor('org.hibernate.orm:hibernate-jpamodelgen:6.6.42.Final')
developmentOnly 'org.springframework.boot:spring-boot-devtools'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -613,4 +613,28 @@ void shouldReturn400WhenCaptureSessionProcessingWithinTimeout() throws JsonProce
);
assertResponseCode(registerResponse, 400);
}

@Test
@DisplayName("Scenario: Try to create Capture Session with unsafe data")
void shouldNotCreateCaptureSessionWithUnsafeData() throws JsonProcessingException {
var res = doPostRequest("/testing-support/create-well-formed-booking", null)
.body()
.jsonPath();
var bookingId = res.getUUID("bookingId");

// create capture session
var dto = createCaptureSession(bookingId);
dto.setStatus(RecordingStatus.RECORDING_AVAILABLE);
dto.setIngestAddress("rtmps://<script>alert(1)</script>.example.com/stream");
dto.setLiveOutputUrl("https://ep-default-live-pre-mediakind-stg<img>src=x onerror=alert(1)>."
+ "blob.core.windows.net/ingest");

var putResponse = putCaptureSession(dto);
assertResponseCode(putResponse, 400);
assertThat(putResponse.body().jsonPath().getString("ingestAddress"))
.contains("potentially malicious content");
assertThat(putResponse.body().jsonPath().getString("liveOutputUrl"))
.contains("potentially malicious content");
assertCaptureSessionExists(dto.getId(), false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@
import org.junit.jupiter.api.Test;
import uk.gov.hmcts.reform.preapi.controllers.params.TestingSupportRoles;
import uk.gov.hmcts.reform.preapi.dto.CourtEmailDTO;
import uk.gov.hmcts.reform.preapi.dto.CreateCourtDTO;
import uk.gov.hmcts.reform.preapi.enums.CourtType;
import uk.gov.hmcts.reform.preapi.exception.NotFoundException;
import uk.gov.hmcts.reform.preapi.util.FunctionalTestBase;

import java.util.List;
import java.util.UUID;

import static java.lang.String.format;
import static org.assertj.core.api.Assertions.assertThat;

class CourtControllerFT extends FunctionalTestBase {

private static final String INPUT_CSV_PATH = "src/functionalTest/resources/test/courts/email_addresses.csv";
private static final String INPUT_CSV_PATH_UNSAFE =
"src/functionalTest/resources/test/courts/email_addresses_unsafe.csv";

@DisplayName("Scenario: Create and update a court")
@Test
Expand Down Expand Up @@ -73,4 +78,53 @@ void updateCourtEmailAddressesFromCsv() throws JsonProcessingException {
assertThat(updatedCourt.getGroupEmail()).isEqualTo("PRE.Edits.Example@justice.gov.uk");
}

@Test
@DisplayName("Should not update court email addresses from a csv file if unsafe data")
void updateCourtEmailAddressesFromCsvUnsafeData() throws JsonProcessingException {

// create courts
var regionId = doPostRequest("/testing-support/create-region", null)
.body().jsonPath().getUUID("regionId");

var dto = new CreateCourtDTO();
dto.setId(UUID.randomUUID());
dto.setName("Examples Court");
dto.setCourtType(CourtType.CROWN);
dto.setRegions(List.of(regionId));
dto.setLocationCode("113456789");

var createResponse = putCourt(dto);
assertResponseCode(createResponse, 201);
var courtResponse1 = assertCourtExists(dto.getId(), true);
assertThat(courtResponse1.body().jsonPath().getString("name")).isEqualTo(dto.getName());

Response postResponse = doPostRequestWithMultipart(
COURTS_ENDPOINT + "/email",
MULTIPART_HEADERS,
INPUT_CSV_PATH_UNSAFE,
TestingSupportRoles.SUPER_USER
);

assertResponseCode(postResponse, 400);
assertThat(postResponse.getBody().asPrettyString())
.contains("potentially malicious content");
}

@Test
@DisplayName("Should not save court that has unsafe data in fields")
void createCourtWithUnsanitizedFields() throws JsonProcessingException {
CreateCourtDTO dto = createCourt();
dto.setName("<script>alert('XSS')</script>Rejected");
dto.setLocationCode("<img src=x onerror='alert(1)'>");
dto.setCounty("<b>Admin User</b>");

Response response = putCourt(dto);
assertResponseCode(response, 400);
assertThat(response.body().jsonPath().getString("name"))
.contains("potentially malicious content");
assertThat(response.body().jsonPath().getString("locationCode"))
.contains("potentially malicious content");
assertThat(response.body().jsonPath().getString("county"))
.contains("potentially malicious content");
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package uk.gov.hmcts.reform.preapi.controllers;

import com.fasterxml.jackson.core.JsonProcessingException;
import io.restassured.response.Response;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.NullSource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import uk.gov.hmcts.reform.preapi.controllers.params.TestingSupportRoles;
import uk.gov.hmcts.reform.preapi.dto.CreateEditRequestDTO;
import uk.gov.hmcts.reform.preapi.dto.EditCutInstructionDTO;
import uk.gov.hmcts.reform.preapi.dto.EditRequestDTO;
import uk.gov.hmcts.reform.preapi.dto.FfmpegEditInstructionDTO;
Expand All @@ -16,13 +18,16 @@
import uk.gov.hmcts.reform.preapi.media.storage.AzureFinalStorageService;
import uk.gov.hmcts.reform.preapi.util.FunctionalTestBase;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

public class EditControllerFT extends FunctionalTestBase {
private static final String VALID_EDIT_CSV = "src/functionalTest/resources/test/edit/edit_from_csv.csv";
private static final String UNSAFE_EDIT_CSV = "src/functionalTest/resources/test/edit/edit_from_csv_unsafe.csv";
private static final String EDIT_ENDPOINT = "/edits";

@MockitoBean
Expand Down Expand Up @@ -96,4 +101,61 @@ void editRequestFromCsvForbidden(TestingSupportRoles role) {
);
assertResponseCode(response, role == null ? 401 : 403);
}

@Test
@DisplayName("Should not create an edit with a csv that has unsafe data in fields")
void editRequestWithUnsafeDataCsv() throws JsonProcessingException {
CreateRecordingResponse recordingDetails = createRecording();
RecordingDTO recordingDTO = assertRecordingExists(recordingDetails.recordingId(), true).as(RecordingDTO.class);

when(azureFinalStorageService.getMp4FileName(recordingDetails.recordingId().toString()))
.thenReturn(recordingDTO.getFilename());
when(azureFinalStorageService.getRecordingDuration(recordingDetails.recordingId()))
.thenReturn(recordingDTO.getDuration());

Response postResponse = doPostRequestWithMultipart(
EDIT_ENDPOINT + "/from-csv/" + recordingDetails.recordingId(),
MULTIPART_HEADERS,
UNSAFE_EDIT_CSV,
TestingSupportRoles.SUPER_USER
);

assertResponseCode(postResponse, 400);
}

@SuppressWarnings("checkstyle:VariableDeclarationUsageDistance")
@Test
@DisplayName("Should not create an edit request with unsafe data in fields")
void editRequestWithUnsafeData() throws JsonProcessingException {
UUID editRequestId = UUID.randomUUID();
CreateRecordingResponse recordingDetails = createRecording();

CreateEditRequestDTO editRequestDTO = new CreateEditRequestDTO();
editRequestDTO.setSourceRecordingId(recordingDetails.recordingId());
editRequestDTO.setStatus(EditRequestStatus.PENDING);
editRequestDTO.setId(editRequestId);
List<EditCutInstructionDTO> editCutInstructionDTOS = new ArrayList<>();
EditCutInstructionDTO cutInstruction1 = new EditCutInstructionDTO();
cutInstruction1.setStartOfCut("00:00:00");
cutInstruction1.setEndOfCut("00:01:00");
cutInstruction1.setReason("<script>alert('XSS')</script>Unsafe reason");
editCutInstructionDTOS.add(cutInstruction1);
editRequestDTO.setEditInstructions(editCutInstructionDTOS);

RecordingDTO recordingDTO = assertRecordingExists(recordingDetails.recordingId(), true).as(RecordingDTO.class);
when(azureFinalStorageService.getMp4FileName(recordingDetails.recordingId().toString()))
.thenReturn(recordingDTO.getFilename());
when(azureFinalStorageService.getRecordingDuration(recordingDetails.recordingId()))
.thenReturn(recordingDTO.getDuration());

Response postResponse = doPutRequest(
EDIT_ENDPOINT + "/" + editRequestId,
OBJECT_MAPPER.writeValueAsString(editRequestDTO),
TestingSupportRoles.SUPER_USER
);

assertResponseCode(postResponse, 400);
assertThat(postResponse.getBody().asPrettyString())
.contains("potentially malicious content");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import static org.assertj.core.api.Assertions.assertThat;

class InviteControllerFT extends FunctionalTestBase {

@DisplayName("Create a portal invite for new user")
@Test
void createPortalInvite() throws JsonProcessingException {
Expand Down Expand Up @@ -168,6 +169,22 @@ void deleteInviteForNonExistingUser() throws JsonProcessingException {
assertResponseCode(deleteResponse, 404);
}

@DisplayName("Create a portal invite with unsafe data in fields")
@Test
void createPortalInviteWithUnSafeFields() throws JsonProcessingException {
var dto = createInvite(null);
dto.setOrganisation("<a href=\"http://example.com\">Malicious Link</a>");
dto.setFirstName("<script>alert('XSS')</script>Example");
dto.setLastName("<b>hello</b>");
assertUserExists(dto.getUserId(), false);

var putResponse = putInvite(dto);
assertResponseCode(putResponse, 400);
assertThat(putResponse.body().jsonPath().getString("organisation"))
.contains("potentially malicious content");
assertInviteExists(dto.getUserId(), false);
}

private CreateInviteDTO createInvite(UUID userId) {
var dto = new CreateInviteDTO();
dto.setUserId(userId != null ? userId : UUID.randomUUID());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,26 @@ void userFilteredByAppActiveStatus() throws JsonProcessingException {
.isEqualTo(0);
}

@DisplayName("Scenario: Should not create/update a user with un-sanitised data")
@Test
void shouldNoCreateUserWithUnsafeData() throws JsonProcessingException {
var dto = createUserDto();
dto.setOrganisation("<script>alert(1)</script>");
dto.setFirstName("<br>First</br>");
dto.setLastName("<img src='x' onerror='alert(1)'>Last</img>");
dto.setPhoneNumber("<script>alert(1)</script>");
var createResponse = putUser(dto);
assertResponseCode(createResponse, 400);
assertThat(createResponse.body().jsonPath().getString("organisation"))
.contains("potentially malicious content");
assertThat(createResponse.body().jsonPath().getString("firstName"))
.contains("potentially malicious content");
assertThat(createResponse.body().jsonPath().getString("lastName"))
.contains("potentially malicious content");
assertThat(createResponse.body().jsonPath().getString("phoneNumber"))
.contains("potentially malicious content");
}

private UserDTO getUserById(UUID userId) {
Response response = doGetRequest(USERS_ENDPOINT + "/" + userId, TestingSupportRoles.SUPER_USER);
assertResponseCode(response, 200);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import io.restassured.response.Response;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.NullSource;
Expand All @@ -14,6 +15,8 @@

import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

public class VfMigrationControllerFT extends FunctionalTestBase {
private static final String MIGRATION_RECORD_ENDPOINT = "/vf-migration-records";

Expand Down Expand Up @@ -48,6 +51,30 @@ void putMigrationRecordsAuth(TestingSupportRoles role) throws JsonProcessingExce
assertResponseCode(response, role == null ? 401 : 403);
}

@Test
@DisplayName("Should not allow a migration record to be updated with unsafe data")
void putMigrationRecordsAuthWithUnsafeData() throws JsonProcessingException {
CreateVfMigrationRecordDTO dto = new CreateVfMigrationRecordDTO();
dto.setId(UUID.randomUUID());
dto.setStatus(VfMigrationStatus.PENDING);
dto.setRecordingVersion(VfMigrationRecordingVersion.ORIG);
dto.setUrn("1234567890");
dto.setExhibitReference("1234567890");
dto.setCourtId(UUID.randomUUID());
dto.setWitnessName("<script></script>");
dto.setDefendantName("<img src='x'>");
Response response = doPutRequest(
MIGRATION_RECORD_ENDPOINT + "/" + dto.getId(),
OBJECT_MAPPER.writeValueAsString(dto),
TestingSupportRoles.SUPER_USER
);
assertResponseCode(response, 400);
assertThat(response.body().jsonPath().getString("witnessName"))
.contains("potentially malicious content");
assertThat(response.body().jsonPath().getString("defendantName"))
.contains("potentially malicious content");
}

@NullSource
@ParameterizedTest
@EnumSource(value = TestingSupportRoles.class, names = "SUPER_USER", mode = EnumSource.Mode.EXCLUDE)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Region,Court,PRE Inbox Address
<img src='x' onerror='alert(1)'>Last</img>,Examples Court,<img src='x' onerror='alert(1)'>Last</img>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Edit Number,Start time of cut,End time of cut,Total time removed,Reason
1,00:00:00,00:01:00,00:01:00,<script></script>
2,00:01:01,00:02:00,00:00:59,<img></img>
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.NoArgsConstructor;
import uk.gov.hmcts.reform.preapi.dto.validators.SanitizedStringConstraint;
import uk.gov.hmcts.reform.preapi.entities.Audit;
import uk.gov.hmcts.reform.preapi.enums.AuditLogSource;

Expand Down Expand Up @@ -40,14 +41,16 @@ public class CreateAuditDTO {
private String category;

@Schema(description = "AuditActivity", examples = {"Create", "Update", "Delete", "Check", "Play", "Locked"})
@SanitizedStringConstraint
private String activity;

@Schema(description = "AuditFunctionalArea", examples = {"Registration", "Login", "Video Player", "API", "Admin"})
@SanitizedStringConstraint
private String functionalArea;

@Schema(description = "AuditDetailsJSONString")
@JsonRawValue
private JsonNode auditDetails;
private JsonNode auditDetails; //TODO: Sanitised annotation to be added later

public CreateAuditDTO(Audit auditEntity) {
this.id = auditEntity.getId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.NoArgsConstructor;
import uk.gov.hmcts.reform.preapi.dto.validators.SanitizedStringConstraint;
import uk.gov.hmcts.reform.preapi.entities.CaptureSession;
import uk.gov.hmcts.reform.preapi.enums.RecordingOrigin;
import uk.gov.hmcts.reform.preapi.enums.RecordingStatus;
Expand All @@ -31,9 +32,11 @@ public class CreateCaptureSessionDTO {
private RecordingOrigin origin;

@Schema(description = "CreateCaptureSessionIngestAddress")
@SanitizedStringConstraint
private String ingestAddress;

@Schema(description = "CreateCaptureSessionLiveOutputURL")
@SanitizedStringConstraint
private String liveOutputUrl;

@Schema(description = "CreateCaptureSessionStartedAt")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.NoArgsConstructor;
import uk.gov.hmcts.reform.preapi.dto.validators.SanitizedStringConstraint;
import uk.gov.hmcts.reform.preapi.enums.CourtType;

import java.util.List;
Expand All @@ -22,6 +23,7 @@ public class CreateCourtDTO {
@NotNull(message = "id is required")
private UUID id;

@SanitizedStringConstraint
@Schema(description = "CreateCourtName")
@NotNull(message = "name is required")
private String name;
Expand All @@ -30,10 +32,12 @@ public class CreateCourtDTO {
@NotNull(message = "court_type is required")
private CourtType courtType;

@SanitizedStringConstraint
@Schema(description = "CreateCourtLocationCode")
@NotNull(message = "location_code is required")
private String locationCode;

@SanitizedStringConstraint
@Schema(description = "CreateCourtCounty")
private String county;

Expand Down
Loading
Loading