fix(security): use server-configured PET notification recipient
This commit is contained in:
@@ -47,9 +47,8 @@ export const sendContactUsConfirmation = async (formValues, language) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const sendContactUsPETNotification = async (formValues, petEmail) => {
|
||||
export const sendContactUsPETNotification = async (formValues) => {
|
||||
const mailData = {
|
||||
"petEmail": petEmail,
|
||||
"contactEmail": formValues.contactEmail,
|
||||
"contactSubject": formValues.contactSubject,
|
||||
"contactbody": formValues.contactbody,
|
||||
|
||||
@@ -15,7 +15,6 @@ const allowedSubjects = [
|
||||
];
|
||||
|
||||
const allowedFields = new Set([
|
||||
"petEmail",
|
||||
"contactEmail",
|
||||
"contactSubject",
|
||||
"contactbody",
|
||||
@@ -120,22 +119,22 @@ export default async function ApiProxy(req, res) {
|
||||
);
|
||||
}
|
||||
|
||||
const petEmail = sanitizeString(requestBody.petEmail);
|
||||
const petEmail = sanitizeString(process.env.PETEMAIL);
|
||||
const contactEmail = sanitizeString(requestBody.contactEmail);
|
||||
const contactSubject = sanitizeString(requestBody.contactSubject);
|
||||
const contactBody = requestBody.contactbody;
|
||||
|
||||
if (
|
||||
typeof requestBody.petEmail !== "string" ||
|
||||
typeof process.env.PETEMAIL !== "string" ||
|
||||
!isNonEmptyString(petEmail) ||
|
||||
petEmail.length > 200 ||
|
||||
!emailRegex.test(petEmail)
|
||||
) {
|
||||
return validationError(
|
||||
res,
|
||||
"INVALID_PET_EMAIL",
|
||||
"petEmail must be a valid email address of 200 characters or fewer"
|
||||
);
|
||||
return respondError(res, {
|
||||
status: 500,
|
||||
code: "PET_RECIPIENT_NOT_CONFIGURED",
|
||||
message: "PET notification recipient is not configured"
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
+3
-17
@@ -21,7 +21,6 @@ import {
|
||||
sendContactUsConfirmation,
|
||||
sendContactUsPETNotification
|
||||
} from "../actions/services/notifyDirectService";
|
||||
import { wrapper } from "../store/store";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -322,7 +321,7 @@ let ContactUs = (props) => {
|
||||
sendContactEmail(formValues);
|
||||
createCRMTask(formValues);
|
||||
formValues.typeOfContact == "plq" &&
|
||||
sendPETEmail(formValues, props.petEmail);
|
||||
sendPETEmail(formValues);
|
||||
console.log("Form submitted:", formValues);
|
||||
};
|
||||
|
||||
@@ -350,11 +349,9 @@ let ContactUs = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const sendPETEmail = async (formValues, petEmail) => {
|
||||
console.log(petEmail);
|
||||
|
||||
const sendPETEmail = async (formValues) => {
|
||||
try {
|
||||
const res = await sendContactUsPETNotification(formValues, petEmail);
|
||||
const res = await sendContactUsPETNotification(formValues);
|
||||
|
||||
console.log("Email sent successfully.");
|
||||
setFormSubmitted(true);
|
||||
@@ -519,17 +516,6 @@ const mapStateToProps = (state) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getServerSideProps = wrapper.getServerSideProps(
|
||||
(store) => async (ctx) => {
|
||||
const { req, locale } = ctx;
|
||||
return {
|
||||
props: {
|
||||
petEmail: process.env.PETEMAIL
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
sendContactUsConfirmation,
|
||||
sendContactUsPETNotification
|
||||
} from "../../actions/services/notifyDirectService";
|
||||
import { wrapper } from "../../store/store";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -322,7 +321,7 @@ let ContactUs = (props) => {
|
||||
sendContactEmail(formValues);
|
||||
createCRMTask(formValues);
|
||||
formValues.typeOfContact == "plq" &&
|
||||
sendPETEmail(formValues, props.petEmail);
|
||||
sendPETEmail(formValues);
|
||||
console.log("Form submitted:", formValues);
|
||||
};
|
||||
|
||||
@@ -350,11 +349,9 @@ let ContactUs = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const sendPETEmail = async (formValues, petEmail) => {
|
||||
console.log(petEmail);
|
||||
|
||||
const sendPETEmail = async (formValues) => {
|
||||
try {
|
||||
const res = await sendContactUsPETNotification(formValues, petEmail);
|
||||
const res = await sendContactUsPETNotification(formValues);
|
||||
|
||||
console.log("Email sent successfully.");
|
||||
setFormSubmitted(true);
|
||||
@@ -520,17 +517,6 @@ const mapStateToProps = (state) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getServerSideProps = wrapper.getServerSideProps(
|
||||
(store) => async (ctx) => {
|
||||
const { req, locale } = ctx;
|
||||
return {
|
||||
props: {
|
||||
petEmail: process.env.PETEMAIL
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
|
||||
@@ -436,8 +436,10 @@ const loadContactConfirmationEndpoint = ({ sendEmail } = {}) => {
|
||||
const withContactOrigins = async (fn) => {
|
||||
const previousNextAuthUrl = process.env.NEXTAUTH_URL;
|
||||
const previousCyApiRoot = process.env.CY_API_ROOT;
|
||||
const previousPetEmail = process.env.PETEMAIL;
|
||||
process.env.NEXTAUTH_URL = trustedContactOrigin;
|
||||
process.env.CY_API_ROOT = trustedWelshContactOrigin;
|
||||
process.env.PETEMAIL = "pet@example.com";
|
||||
|
||||
try {
|
||||
await fn();
|
||||
@@ -447,6 +449,9 @@ const withContactOrigins = async (fn) => {
|
||||
|
||||
if (previousCyApiRoot === undefined) delete process.env.CY_API_ROOT;
|
||||
else process.env.CY_API_ROOT = previousCyApiRoot;
|
||||
|
||||
if (previousPetEmail === undefined) delete process.env.PETEMAIL;
|
||||
else process.env.PETEMAIL = previousPetEmail;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -775,7 +780,6 @@ const validContactPetRequest = (overrides = {}) => ({
|
||||
"content-type": "application/json; charset=utf-8"
|
||||
},
|
||||
body: {
|
||||
petEmail: "pet@example.com",
|
||||
contactEmail: "applicant@example.com",
|
||||
contactSubject: contactSubjects.plq.en,
|
||||
contactbody: "Please help with my enquiry.",
|
||||
@@ -931,18 +935,83 @@ test("Contact Us PET notification enforces its exact field set", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
for (const petEmail of [
|
||||
test("Contact Us PET notification rejects obsolete caller-supplied recipient", async () => {
|
||||
await assertContactPetRejected({
|
||||
req: validContactPetRequest({
|
||||
body: {
|
||||
...validContactPetRequest().body,
|
||||
petEmail: "pet@example.com"
|
||||
}
|
||||
}),
|
||||
status: 400,
|
||||
code: "INVALID_REQUEST_FIELDS"
|
||||
});
|
||||
});
|
||||
|
||||
for (const configuredRecipient of [undefined, "invalid"]) {
|
||||
test(`Contact Us PET notification fails safely for configured recipient ${String(configuredRecipient)}`, async () => {
|
||||
await withContactOrigins(async () => {
|
||||
if (configuredRecipient === undefined) delete process.env.PETEMAIL;
|
||||
else process.env.PETEMAIL = configuredRecipient;
|
||||
|
||||
const loaded = loadContactPetEndpoint();
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default(validContactPetRequest(), res);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 500);
|
||||
assert.strictEqual(
|
||||
res.state.jsonBody.error.code,
|
||||
"PET_RECIPIENT_NOT_CONFIGURED"
|
||||
);
|
||||
assert.strictEqual(loaded.notifyCalls.length, 0);
|
||||
assert.strictEqual(
|
||||
JSON.stringify(res.state.jsonBody).includes(
|
||||
String(configuredRecipient)
|
||||
),
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("Contact Us PET notification rejects caller recipient before configured recipient validation", async () => {
|
||||
await withContactOrigins(async () => {
|
||||
delete process.env.PETEMAIL;
|
||||
const loaded = loadContactPetEndpoint();
|
||||
const res = createRes();
|
||||
|
||||
await loaded.mod.default(
|
||||
validContactPetRequest({
|
||||
body: {
|
||||
...validContactPetRequest().body,
|
||||
petEmail: "attacker@example.com"
|
||||
}
|
||||
}),
|
||||
res
|
||||
);
|
||||
|
||||
assert.strictEqual(res.state.statusCode, 400);
|
||||
assert.strictEqual(
|
||||
res.state.jsonBody.error.code,
|
||||
"INVALID_REQUEST_FIELDS"
|
||||
);
|
||||
assert.strictEqual(loaded.notifyCalls.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
for (const contactEmail of [
|
||||
undefined,
|
||||
"invalid",
|
||||
`${"a".repeat(190)}@example.com`
|
||||
]) {
|
||||
test(`Contact Us PET notification rejects recipient ${String(petEmail).slice(0, 30)}`, async () => {
|
||||
test(`Contact Us PET notification rejects contact email ${String(contactEmail).slice(0, 30)}`, async () => {
|
||||
await assertContactPetRejected({
|
||||
req: validContactPetRequest({
|
||||
body: { ...validContactPetRequest().body, petEmail }
|
||||
body: { ...validContactPetRequest().body, contactEmail }
|
||||
}),
|
||||
status: 400,
|
||||
code: "INVALID_PET_EMAIL"
|
||||
code: "INVALID_CONTACT_EMAIL"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -480,10 +480,7 @@ test("notify/sendContactUsPETNotification posts only the dedicated PET payload",
|
||||
ignored: "not sent"
|
||||
};
|
||||
|
||||
const result = await notify.sendContactUsPETNotification(
|
||||
formValues,
|
||||
"pet@example.com"
|
||||
);
|
||||
const result = await notify.sendContactUsPETNotification(formValues);
|
||||
|
||||
assert.deepStrictEqual(normalize(result), { sent: true });
|
||||
assert.strictEqual(
|
||||
@@ -492,7 +489,6 @@ test("notify/sendContactUsPETNotification posts only the dedicated PET payload",
|
||||
);
|
||||
assert.strictEqual(axios.calls[0].config.method, "post");
|
||||
assert.deepStrictEqual(normalize(axios.calls[0].config.data), {
|
||||
petEmail: "pet@example.com",
|
||||
contactEmail: "person@example.com",
|
||||
contactSubject: "Questions about Planning legislation or policy",
|
||||
contactbody: "Please review https://example.invalid",
|
||||
@@ -513,15 +509,12 @@ test("notify/sendContactUsPETNotification logs and rethrows on failure", async (
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
notify.sendContactUsPETNotification(
|
||||
{
|
||||
contactEmail: "person@example.com",
|
||||
contactSubject: "Questions about Planning legislation or policy",
|
||||
contactbody: "Message",
|
||||
typeOfContact: "plq"
|
||||
},
|
||||
"pet@example.com"
|
||||
),
|
||||
notify.sendContactUsPETNotification({
|
||||
contactEmail: "person@example.com",
|
||||
contactSubject: "Questions about Planning legislation or policy",
|
||||
contactbody: "Message",
|
||||
typeOfContact: "plq"
|
||||
}),
|
||||
(thrown) => thrown === error
|
||||
);
|
||||
assert.strictEqual(logger.calls.length, 1);
|
||||
@@ -538,7 +531,7 @@ test("public and portal Contact Us pages preserve PET gating and submission orde
|
||||
'formValues.typeOfContact == "plq" &&'
|
||||
);
|
||||
const petCall = source.indexOf(
|
||||
"sendPETEmail(formValues, props.petEmail);"
|
||||
"sendPETEmail(formValues);"
|
||||
);
|
||||
|
||||
assert.notStrictEqual(confirmationCall, -1);
|
||||
@@ -552,9 +545,7 @@ test("public and portal Contact Us pages preserve PET gating and submission orde
|
||||
true
|
||||
);
|
||||
assert.strictEqual(
|
||||
source.includes(
|
||||
"sendContactUsPETNotification(formValues, petEmail)"
|
||||
),
|
||||
source.includes("sendContactUsPETNotification(formValues)"),
|
||||
true
|
||||
);
|
||||
assert.strictEqual(
|
||||
@@ -562,6 +553,8 @@ test("public and portal Contact Us pages preserve PET gating and submission orde
|
||||
true
|
||||
);
|
||||
assert.strictEqual(source.includes("sendEmail(templateId,"), false);
|
||||
assert.strictEqual(source.includes("props.petEmail"), false);
|
||||
assert.strictEqual(source.includes("process.env.PETEMAIL"), false);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user