From 245f97fa73b384ebf96f42bea931e693440d9756 Mon Sep 17 00:00:00 2001 From: Nafees Nehar Date: Fri, 24 May 2024 02:02:52 +0530 Subject: [PATCH] [ENGG-1692] feat: warning for CSP errors in dynamic response rules (#1726) --- browser-extension/common/src/constants.ts | 2 + .../client-scripts/ajaxRequestInterceptor.js | 144 +++++++++++------- .../client/pageScriptMessageListener.ts | 11 ++ .../service-worker/services/messageHandler.ts | 4 + .../requestProcessor/handleCSPError.ts | 30 ++++ .../services/requestProcessor/index.ts | 13 +- .../services/requestProcessor/types.ts | 1 + 7 files changed, 152 insertions(+), 53 deletions(-) create mode 100644 browser-extension/mv3/src/service-worker/services/requestProcessor/handleCSPError.ts diff --git a/browser-extension/common/src/constants.ts b/browser-extension/common/src/constants.ts index 2931fb79c8..4a10669b8c 100644 --- a/browser-extension/common/src/constants.ts +++ b/browser-extension/common/src/constants.ts @@ -27,6 +27,7 @@ export const EXTENSION_MESSAGES = { INIT_SESSION_RECORDER: "initSessionRecorder", CLIENT_PAGE_LOADED: "clientPageLoaded", ON_BEFORE_AJAX_REQUEST: "onBeforeAjaxRequest", + ON_ERROR_OCCURRED: "onErrorOccurred", SAVE_TEST_RULE_RESULT: "saveTestRuleResult", NOTIFY_TEST_RULE_REPORT_UPDATED: "notifyTestRuleReportUpdated", TEST_RULE_ON_URL: "testRuleOnUrl", @@ -49,6 +50,7 @@ export const CLIENT_MESSAGES = { NOTIFY_RECORD_UPDATED_IN_POPUP: "notifyRecordUpdatedInPopup", NOTIFY_PAGE_LOADED_FROM_CACHE: "notifyPageLoadedFromCache", ON_BEFORE_AJAX_REQUEST_PROCESSED: "onBeforeAjaxRequest:processed", + ON_ERROR_OCCURRED_PROCESSED: "onErrorOccurred:processed", START_EXPLICIT_RULE_TESTING: "startExplicitRuleTesting", START_IMPLICIT_RULE_TESTING: "startImplicitRuleTesting", SYNC_APPLIED_RULES: "syncAppliedRules", diff --git a/browser-extension/mv3/src/client-scripts/ajaxRequestInterceptor.js b/browser-extension/mv3/src/client-scripts/ajaxRequestInterceptor.js index 713fed65d3..e3778801ec 100644 --- a/browser-extension/mv3/src/client-scripts/ajaxRequestInterceptor.js +++ b/browser-extension/mv3/src/client-scripts/ajaxRequestInterceptor.js @@ -183,8 +183,26 @@ import { PUBLIC_NAMESPACE } from "common/constants"; return responseModification.type === "static" && responseModification.serveWithoutRequest; }; - const getFunctionFromCode = (code) => { - return new Function("args", `return (${code})(args);`); + let logShown = false; + const getFunctionFromCode = (code, ruleType) => { + try { + return new Function("args", `return (${code})(args);`); + } catch (e) { + notifyOnErrorOccurred({ + initiatorDomain: location.origin, + url: location.href, + }).then(() => { + if (!logShown) { + logShown = true; + console.log( + `%cRequestly%c Please reload the page for ${ruleType} rule to take effect`, + "color: #3c89e8; padding: 1px 5px; border-radius: 4px; border: 1px solid #91caff;", + "color: red; font-style: italic" + ); + } + }); + return () => {}; + } }; const getCustomRequestBody = (requestRuleData, args) => { @@ -192,7 +210,7 @@ import { PUBLIC_NAMESPACE } from "common/constants"; if (requestRuleData.request.type === "static") { requestBody = requestRuleData.request.value; } else { - requestBody = getFunctionFromCode(requestRuleData.request.value)(args); + requestBody = getFunctionFromCode(requestRuleData.request.value, "request")(args); } if (typeof requestBody !== "object" || isNonJsonObject(requestBody)) { @@ -276,35 +294,43 @@ import { PUBLIC_NAMESPACE } from "common/constants"; const isContentTypeJSON = (contentType) => !!contentType?.includes("application/json"); - const notifyOnBeforeRequest = async (requestDetails) => { + const postMessageAndWaitForAck = async (message, action) => { window.postMessage( { + ...message, + action, source: "requestly:client", - action: "onBeforeAjaxRequest", - requestDetails, }, window.location.href ); - let onBeforeAjaxRequestAckHandler; + let ackHandler; + + const ackAction = `${action}:processed`; return Promise.race([ + new Promise((resolve) => setTimeout(resolve, 2000)), new Promise((resolve) => { - setTimeout(resolve, 2000); - }), - new Promise((resolve) => { - onBeforeAjaxRequestAckHandler = (event) => { - if (event.data.action === "onBeforeAjaxRequest:processed") { + ackHandler = (event) => { + if (event.data.action === ackAction) { resolve(); } }; - window.addEventListener("message", onBeforeAjaxRequestAckHandler); + window.addEventListener("message", ackHandler); }), ]).finally(() => { - window.removeEventListener("message", onBeforeAjaxRequestAckHandler); + window.removeEventListener("message", ackHandler); }); }; + const notifyOnBeforeRequest = async (requestDetails) => { + return postMessageAndWaitForAck({ requestDetails }, "onBeforeAjaxRequest"); + }; + + const notifyOnErrorOccurred = async (requestDetails) => { + return postMessageAndWaitForAck({ requestDetails }, "onErrorOccurred"); + }; + /** * ********** Within Context Functions end here ************* */ @@ -359,7 +385,10 @@ import { PUBLIC_NAMESPACE } from "common/constants"; if (this.readyState === this.DONE) { let customResponse = responseModification.type === "code" - ? getFunctionFromCode(responseRuleData.response.value)({ + ? getFunctionFromCode( + responseRuleData.response.value, + "response" + )({ method: this.method, url, requestHeaders: this.requestHeaders, @@ -370,6 +399,10 @@ import { PUBLIC_NAMESPACE } from "common/constants"; }) : responseModification.value; + if (typeof customResponse === "undefined") { + return; + } + // Convert customResponse back to rawText // response.value is String and evaluator method might return string/object if (isPromise(customResponse)) { @@ -485,15 +518,19 @@ import { PUBLIC_NAMESPACE } from "common/constants"; bodyAsJson: jsonifyValidJSONString(data), }); - notifyRequestRuleApplied({ - ruleDetails: requestRule, - requestDetails: { - url: this.requestURL, - method: this.method, - type: "xmlhttprequest", - timeStamp: Date.now(), - }, - }); + if (typeof this.requestData !== "undefined") { + notifyRequestRuleApplied({ + ruleDetails: requestRule, + requestDetails: { + url: this.requestURL, + method: this.method, + type: "xmlhttprequest", + timeStamp: Date.now(), + }, + }); + } else { + this.requestData = data; + } } this.responseRule = getMatchedResponseRule(this.requestURL); @@ -542,36 +579,37 @@ import { PUBLIC_NAMESPACE } from "common/constants"; if (requestRuleData) { const originalRequestBody = await request.text(); - const requestBody = - getCustomRequestBody(requestRuleData, { - method, - url, - body: originalRequestBody, - bodyAsJson: jsonifyValidJSONString(originalRequestBody), - }) || {}; - - request = new Request(request.url, { + const requestBody = getCustomRequestBody(requestRuleData, { method, - body: requestBody, - headers: request.headers, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - mode: request.mode, - credentials: request.credentials, - cache: request.cache, - redirect: request.redirect, - integrity: request.integrity, + url, + body: originalRequestBody, + bodyAsJson: jsonifyValidJSONString(originalRequestBody), }); - notifyRequestRuleApplied({ - ruleDetails: requestRuleData, - requestDetails: { - url, + if (typeof requestBody !== undefined) { + request = new Request(request.url, { method, - type: "fetch", - timeStamp: Date.now(), - }, - }); + body: requestBody ?? {}, + headers: request.headers, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + mode: request.mode, + credentials: request.credentials, + cache: request.cache, + redirect: request.redirect, + integrity: request.integrity, + }); + + notifyRequestRuleApplied({ + ruleDetails: requestRuleData, + requestDetails: { + url, + method, + type: "fetch", + timeStamp: Date.now(), + }, + }); + } } let requestData; @@ -682,7 +720,11 @@ import { PUBLIC_NAMESPACE } from "common/constants"; }; } - customResponse = getFunctionFromCode(responseRuleData.response.value)(evaluatorArgs); + customResponse = getFunctionFromCode(responseRuleData.response.value, "response")(evaluatorArgs); + + if (typeof customResponse === "undefined") { + return fetchedResponse; + } // evaluator might return us Object but response.value is string // So make the response consistent by converting to JSON String and then create the Response object diff --git a/browser-extension/mv3/src/content-scripts/client/pageScriptMessageListener.ts b/browser-extension/mv3/src/content-scripts/client/pageScriptMessageListener.ts index 350a97889c..2a3e1590b0 100644 --- a/browser-extension/mv3/src/content-scripts/client/pageScriptMessageListener.ts +++ b/browser-extension/mv3/src/content-scripts/client/pageScriptMessageListener.ts @@ -27,6 +27,17 @@ export const initPageScriptMessageListener = () => { ); }); break; + case EXTENSION_MESSAGES.ON_ERROR_OCCURRED: + chrome.runtime.sendMessage(event.data, () => { + window.postMessage( + { + source: "requestly:client", + action: CLIENT_MESSAGES.ON_ERROR_OCCURRED_PROCESSED, + }, + window.location.href + ); + }); + break; } }); }; diff --git a/browser-extension/mv3/src/service-worker/services/messageHandler.ts b/browser-extension/mv3/src/service-worker/services/messageHandler.ts index d50c528a61..488008e627 100644 --- a/browser-extension/mv3/src/service-worker/services/messageHandler.ts +++ b/browser-extension/mv3/src/service-worker/services/messageHandler.ts @@ -116,6 +116,10 @@ export const initMessageHandler = () => { requestProcessor.onBeforeAJAXRequest(sender.tab.id, message.requestDetails).then(sendResponse); return true; + case EXTENSION_MESSAGES.ON_ERROR_OCCURRED: + requestProcessor.onErrorOccurred(sender.tab.id, message.requestDetails).then(sendResponse); + return true; + case EXTENSION_MESSAGES.TEST_RULE_ON_URL: launchUrlAndStartRuleTesting(message, sender.tab.id); break; diff --git a/browser-extension/mv3/src/service-worker/services/requestProcessor/handleCSPError.ts b/browser-extension/mv3/src/service-worker/services/requestProcessor/handleCSPError.ts new file mode 100644 index 0000000000..1378b16f47 --- /dev/null +++ b/browser-extension/mv3/src/service-worker/services/requestProcessor/handleCSPError.ts @@ -0,0 +1,30 @@ +import { AJAXRequestDetails, SessionRuleType } from "./types"; +import { updateRequestSpecificRules } from "../rulesManager"; + +export const handleCSPError = async (tabId: number, requestDetails: AJAXRequestDetails): Promise => { + await updateRequestSpecificRules( + tabId, + requestDetails.initiatorDomain, + { + action: { + type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS, + responseHeaders: [ + { + header: "Content-Security-Policy", + operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE, + }, + ], + }, + condition: { + urlFilter: requestDetails.initiatorDomain, + resourceTypes: [ + chrome.declarativeNetRequest.ResourceType.SUB_FRAME, + chrome.declarativeNetRequest.ResourceType.MAIN_FRAME, + ], + tabIds: [tabId], + excludedInitiatorDomains: ["requestly.io", "requestly.com"], + }, + }, + SessionRuleType.CSP_ERROR + ); +}; diff --git a/browser-extension/mv3/src/service-worker/services/requestProcessor/index.ts b/browser-extension/mv3/src/service-worker/services/requestProcessor/index.ts index da38e50faf..4045a8e8c5 100644 --- a/browser-extension/mv3/src/service-worker/services/requestProcessor/index.ts +++ b/browser-extension/mv3/src/service-worker/services/requestProcessor/index.ts @@ -3,6 +3,7 @@ import { forwardHeadersOnRedirect } from "./handleHeadersOnRedirect"; import { handleInitiatorDomainFunction } from "./handleInitiatorDomainFunction"; import rulesStorageService from "../../../rulesStorageService"; import { RuleType } from "common/types"; +import { handleCSPError } from "./handleCSPError"; class RequestProcessor { constructor() {} @@ -17,13 +18,21 @@ class RequestProcessor { const redirectReplaceRules = enabledRules.filter( (rule) => rule.ruleType === RuleType.REDIRECT || rule.ruleType === RuleType.REPLACE ); - const headerRules = enabledRules.filter((rule) => rule.ruleType === RuleType.HEADERS); await forwardHeadersOnRedirect(tabId, requestDetails, redirectReplaceRules); - await handleInitiatorDomainFunction(tabId, requestDetails, headerRules); }; + + onErrorOccurred = async (tabId: number, requestDetails: AJAXRequestDetails): Promise => { + const enabledRules = await rulesStorageService.getEnabledRules(); + + if (enabledRules.length === 0) { + return; + } + + await handleCSPError(tabId, requestDetails); + }; } export const requestProcessor = new RequestProcessor(); diff --git a/browser-extension/mv3/src/service-worker/services/requestProcessor/types.ts b/browser-extension/mv3/src/service-worker/services/requestProcessor/types.ts index 8f8a820a27..a84d66b581 100644 --- a/browser-extension/mv3/src/service-worker/services/requestProcessor/types.ts +++ b/browser-extension/mv3/src/service-worker/services/requestProcessor/types.ts @@ -9,4 +9,5 @@ export interface AJAXRequestDetails { export enum SessionRuleType { FORWARD_IGNORED_HEADERS = "forwardIgnoredHeaders", INITIATOR_DOMAIN = "initiatorDomain", + CSP_ERROR = "cspError", }