From a0ae6fcba3b3d3190885986e15a6c924eb57bc98 Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 12 Mar 2026 09:03:24 +0000 Subject: [PATCH] refactor phase 4 hardening helpers and redacted logging --- actions/core/guards.js | 11 +++++ actions/core/logger.js | 44 ++++++++++++++++--- actions/index.js | 1 + pages/api/email/notify.js | 15 ++++++- pages/api/endpoint/createcrmtask_api.js | 22 +++++++--- .../api/endpoint/getpreferredlanguage_api.js | 17 +++++-- 6 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 actions/core/guards.js diff --git a/actions/core/guards.js b/actions/core/guards.js new file mode 100644 index 00000000..b233c126 --- /dev/null +++ b/actions/core/guards.js @@ -0,0 +1,11 @@ +export const isNonEmptyString = (value) => { + return typeof value === "string" && value.trim().length > 0; +}; + +export const sanitizeString = (value) => { + return typeof value === "string" ? value.trim() : ""; +}; + +export const escapeODataString = (value) => { + return sanitizeString(value).replace(/'/g, "''"); +}; diff --git a/actions/core/logger.js b/actions/core/logger.js index 6e3fa4ab..13d35389 100644 --- a/actions/core/logger.js +++ b/actions/core/logger.js @@ -1,5 +1,35 @@ import _ from "lodash"; +const MASK = "[REDACTED]"; + +const redactString = (value) => { + if (typeof value !== "string") return value; + + return value + .replace( + /([a-zA-Z0-9._%+-]{1,})@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g, + `${MASK}@${MASK}` + ) + .replace(/Bearer\s+[A-Za-z0-9._\-]+/gi, `Bearer ${MASK}`) + .replace(/("client_secret"\s*:\s*")[^"]+("?)/gi, `$1${MASK}$2`) + .replace(/("access_token"\s*:\s*")[^"]+("?)/gi, `$1${MASK}$2`); +}; + +export const redactSensitive = (value) => { + if (value == null) return value; + + if (typeof value === "string") { + return redactString(value); + } + + try { + const json = JSON.stringify(value); + return redactString(json); + } catch (_error) { + return MASK; + } +}; + export const getIP = (req) => { const forwarded = req.headers["x-forwarded-for"]; @@ -17,7 +47,7 @@ export const getIP = (req) => { export const consoleLogger = (err) => { console.log( "\n\n/////////////////////////////////////////////////\nRaw Error " + - err + + redactSensitive(err) + "\n\n/////////////////////////////////////////////////\n" ); var errStr = @@ -40,22 +70,24 @@ export const consoleLogger = (err) => { "\n" : "") + (_.has(err, "response.data.error.message") - ? "\nMessage: " + err.response.data.error.message + "\n" + ? "\nMessage: " + + redactSensitive(err.response.data.error.message) + + "\n" : "") + (_.has(err, "response.config.url") - ? "\nRequest URL: " + err.config.url + "\n" + ? "\nRequest URL: " + redactSensitive(err.config.url) + "\n" : "") + (_.has(err, "response.headers.date") ? "\nRequest Time: " + err.response.headers.date + "\n" : "") + (_.has(err, "config.url") - ? "\nAxios config url: " + err.config.url + "\n" + ? "\nAxios config url: " + redactSensitive(err.config.url) + "\n" : "") + (_.has(err, "config.url") - ? "\nAxios message url: " + err.message + "\n" + ? "\nAxios message url: " + redactSensitive(err.message) + "\n" : "") + (_.has(err, "config.data") - ? "\nRequest payload: " + err.config.data + "\n" + ? "\nRequest payload: " + redactSensitive(err.config.data) + "\n" : "") + "\n/////////////////////////////////////////////////\n"; diff --git a/actions/index.js b/actions/index.js index 74312dc5..4849f4a1 100644 --- a/actions/index.js +++ b/actions/index.js @@ -3,5 +3,6 @@ export * from "./core/logger"; export * from "./core/hash"; export * from "./core/token"; export * from "./core/headers"; +export * from "./core/guards"; export * from "./services"; diff --git a/pages/api/email/notify.js b/pages/api/email/notify.js index 3266f3fa..c74edcc4 100644 --- a/pages/api/email/notify.js +++ b/pages/api/email/notify.js @@ -25,11 +25,19 @@ * description: Failed */ -import { consoleLogger } from "../../../actions/core/logger"; +import { consoleLogger, redactSensitive } from "../../../actions/core/logger"; +import { isNonEmptyString, sanitizeString } from "../../../actions/core/guards"; import { getPreferredLanguage } from "../../../actions/services/accountService"; export default async function ApiProxy(req, res) { var data = req.body; + const emailAddress = sanitizeString(data?.emailAddress); + + if (!isNonEmptyString(emailAddress)) { + return res.status(400).json({ error: "emailAddress is required" }); + } + + data.emailAddress = emailAddress; if (data?.reference === "PEDW-NEW-CASEREF") { try { @@ -50,7 +58,10 @@ export default async function ApiProxy(req, res) { const notifyClient = new NotifyClient(process.env.NOTIFY_API_KEY); //const emailReplyToId = process.env.EMAIL_REPLY_TO_ID; - console.log("///////////////\n Sending email \ns//////////////", data); + console.log( + "///////////////\n Sending email \ns//////////////", + redactSensitive(data) + ); notifyClient .sendEmail(data.templateId, data.emailAddress, { diff --git a/pages/api/endpoint/createcrmtask_api.js b/pages/api/endpoint/createcrmtask_api.js index b182bff7..72f7efb5 100644 --- a/pages/api/endpoint/createcrmtask_api.js +++ b/pages/api/endpoint/createcrmtask_api.js @@ -44,6 +44,8 @@ import axios from "axios"; import CryptoJS from "crypto-js"; import _ from "lodash"; +import { isNonEmptyString, sanitizeString } from "../../../actions/core/guards"; +import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { getCaseBlob } from "../../../actions/azurestorage"; const WORDKEY = process.env.HASHKEY; @@ -66,6 +68,17 @@ const hashAPIPath = (queryPath) => { export default async function ApiProxy(req, res) { var createTaskBody = req.body; + + const contactEmail = sanitizeString(createTaskBody?.contactEmail); + const contactSubject = sanitizeString(createTaskBody?.contactSubject); + const contactBody = sanitizeString(createTaskBody?.contactbody); + + if (!isNonEmptyString(contactEmail) || !isNonEmptyString(contactSubject)) { + return res.status(400).json({ + error: "contactEmail and contactSubject are required" + }); + } + var queryUrl = "tasks"; var token = await getToken(); @@ -87,8 +100,8 @@ export default async function ApiProxy(req, res) { let teamId = teamMap[contactValue]; const payload = { - "subject": `Contact Us Enquiry - ${createTaskBody.contactSubject}`, - "description": `From: ${createTaskBody.contactEmail}\n\n${createTaskBody.contactbody}`, + "subject": `Contact Us Enquiry - ${contactSubject}`, + "description": `From: ${contactEmail}\n\n${contactBody}`, "scheduledstart": new Date().toISOString(), "scheduledend": new Date( new Date().getTime() + 60 * 60000 * 24 @@ -118,10 +131,7 @@ export default async function ApiProxy(req, res) { const { data } = await axios(config); return res.status(200).json(data); } catch (error) { - console.error( - "CRM Task Creation Error:", - error.response?.data || error - ); + consoleLogger(error); return res .status(400) .json({ error: "Failed to create CRM task", details: error }); diff --git a/pages/api/endpoint/getpreferredlanguage_api.js b/pages/api/endpoint/getpreferredlanguage_api.js index 11706421..14289afa 100644 --- a/pages/api/endpoint/getpreferredlanguage_api.js +++ b/pages/api/endpoint/getpreferredlanguage_api.js @@ -19,7 +19,12 @@ import axios from "axios"; import CryptoJS from "crypto-js"; import _ from "lodash"; import { azureHeaders } from "../../../actions/core/headers"; -import { consoleLogger } from "../../../actions/core/logger"; +import { + escapeODataString, + isNonEmptyString, + sanitizeString +} from "../../../actions/core/guards"; +import { consoleLogger, redactSensitive } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; const WORDKEY = process.env.HASHKEY; @@ -38,15 +43,19 @@ const hashAPIPath = (queryPath) => { }; export default async function ApiProxy(req, res) { - var emailAddress = req.query.emailAddress; + var emailAddress = sanitizeString(req.query.emailAddress); var token = await getToken(); + if (!isNonEmptyString(emailAddress)) { + return res.status(400).json({ error: "emailAddress is required" }); + } + var queryUrl = "contacts?$filter=emailaddress1 eq '" + - emailAddress + + escapeODataString(emailAddress) + "'&$count=true&$select=pinswg_preferredlanguage,contactid"; - console.log(queryUrl); + console.log(redactSensitive(queryUrl)); // var queryUrl = "contacts/?$count=true&$select=emailaddress1, contactid"; var apiResponse = _.isEmpty(req.query)