diff --git a/back/src/adapters/primary/routers/security.e2e.test.ts b/back/src/adapters/primary/routers/security.e2e.test.ts new file mode 100644 index 0000000000..787939dba8 --- /dev/null +++ b/back/src/adapters/primary/routers/security.e2e.test.ts @@ -0,0 +1,115 @@ +import { + AgencyDtoBuilder, + ConventionDtoBuilder, + UnauthenticatedConventionRoutes, + expectHttpResponseToEqual, + unauthenticatedConventionRoutes, +} from "shared"; +import { HttpClient } from "shared-routes"; +import { createSupertestSharedClient } from "shared-routes/supertest"; +import { AppConfigBuilder } from "../../../utils/AppConfigBuilder"; +import { InMemoryGateways, buildTestApp } from "../../../utils/buildTestApp"; + +describe("security e2e", () => { + const peAgency = new AgencyDtoBuilder().withKind("pole-emploi").build(); + + const convention = new ConventionDtoBuilder() + .withAgencyId(peAgency.id) + .withFederatedIdentity({ provider: "peConnect", token: "some-id" }) + .build(); + + let unauthenticatedRequest: HttpClient; + let gateways: InMemoryGateways; + + beforeEach(async () => { + const testApp = await buildTestApp(new AppConfigBuilder().build()); + const request = testApp.request; + + ({ gateways } = testApp); + + unauthenticatedRequest = createSupertestSharedClient( + unauthenticatedConventionRoutes, + request, + ); + + gateways.timeGateway.defaultDate = new Date(); + }); + + describe("check request body for HTML", () => { + it("400 - should throw an error if a request (POST) body parameter contains HTML", async () => { + const response = await unauthenticatedRequest.createConvention({ + body: { + convention: { + ...convention, + businessName: "", + }, + }, + }); + + expectHttpResponseToEqual(response, { + body: { + status: 400, + message: "Invalid request body", + }, + status: 400, + }); + }); + + it("200 - should not throw an error if a request (POST) body parameter does not contain HTML", async () => { + const response = await unauthenticatedRequest.createConvention({ + body: { + convention: { + ...convention, + businessName: "L'amie > caline !", + }, + }, + }); + + expectHttpResponseToEqual(response, { + body: { + id: convention.id, + }, + status: 200, + }); + }); + + it("400 - should throw an error if a request (GET) query parameter contains HTML", async () => { + const response = await unauthenticatedRequest.findSimilarConventions({ + queryParams: { + beneficiaryBirthdate: "1990-01-01", + beneficiaryLastName: "", + codeAppellation: "1234567890", + dateStart: "2021-01-01", + siret: "12345678901234", + }, + }); + + expectHttpResponseToEqual(response, { + body: { + status: 400, + message: "Invalid request body", + }, + status: 400, + }); + }); + + it("200 - should not throw an error if a request (GET) query parameter does not contain HTML", async () => { + const response = await unauthenticatedRequest.findSimilarConventions({ + queryParams: { + beneficiaryBirthdate: "1990-01-01", + beneficiaryLastName: "Bon< { + if (!excludedRoutes.includes(req.originalUrl)) { + if ( + (req.body && doesObjectContainsHTML(req.body)) || + (req.query && doesObjectContainsHTML(req.query)) + ) { + return res.status(400).json({ + status: 400, + message: "Invalid request body", + }); + } + } + next(); +}; diff --git a/back/src/config/bootstrap/server.ts b/back/src/config/bootstrap/server.ts index dae413925e..5d87c8d07e 100644 --- a/back/src/config/bootstrap/server.ts +++ b/back/src/config/bootstrap/server.ts @@ -31,6 +31,7 @@ import { legacyCreateLogger } from "../../utils/logger"; import { AppConfig } from "./appConfig"; import { createAppDependencies } from "./createAppDependencies"; import { Gateways } from "./createGateways"; +import { detectHtmlInParamsMiddleware } from "./detectHtmlInParamsMiddleware"; import { startCrawler } from "./startCrawler"; const logger = legacyCreateLogger(__filename); @@ -74,7 +75,7 @@ export const createApp = async ( } else next(); }); }); - + app.use(detectHtmlInParamsMiddleware); const deps = await createAppDependencies(config); app.use(createSearchRouter(deps)); diff --git a/front/src/core-logic/adapters/Convention/HttpConventionGateway.ts b/front/src/core-logic/adapters/Convention/HttpConventionGateway.ts index 90ae008c63..4117078f9b 100644 --- a/front/src/core-logic/adapters/Convention/HttpConventionGateway.ts +++ b/front/src/core-logic/adapters/Convention/HttpConventionGateway.ts @@ -123,6 +123,7 @@ export class HttpConventionGateway implements ConventionGateway { .then((response) => match(response) .with({ status: 200 }, ({ body }) => body.similarConventionIds) + .with({ status: 400 }, throwBadRequestWithExplicitMessage) .otherwise(otherwiseThrow), ), ); diff --git a/shared/src/convention/convention.routes.ts b/shared/src/convention/convention.routes.ts index f2c82e45a2..252bd2fb58 100644 --- a/shared/src/convention/convention.routes.ts +++ b/shared/src/convention/convention.routes.ts @@ -143,6 +143,7 @@ export const unauthenticatedConventionRoutes = defineRoutes({ queryParamsSchema: findSimilarConventionsParamsSchema, responses: { 200: findSimilarConventionsResponseSchema, + 400: httpErrorSchema, }, }), }); diff --git a/shared/src/utils/string.ts b/shared/src/utils/string.ts index 3372e23290..969838a541 100644 --- a/shared/src/utils/string.ts +++ b/shared/src/utils/string.ts @@ -1,3 +1,5 @@ +import { values } from "ramda"; + export const capitalize = (str: string): string => str[0].toUpperCase() + str.slice(1); @@ -45,3 +47,21 @@ export const removeSpaces = (str: string) => str.replace(/\s/g, ""); export const isStringEmpty = (str: string) => str !== "" && str.trim().length === 0; + +export const doesStringContainsHTML = (possiblyHtmlString: string): boolean => { + const htmlRegex = + /(?:<[!]?(?:(?:[a-z][a-z0-9-]{0,1000})|(?:!--[\s\S]{0,1000}?--))(?:\s{0,1000}[^>]{0,1000})?>\s{0,1000})|(?:", true], + ["some content with an --> ending of html comment", false], + ["some content with an < open tag", false], + ["> < emoji ?", false], + ])(`should return true for "%s"`, (input, expected) => { + expect(doesStringContainsHTML(input)).toBe(expected); + }); + }); });