From 9ec02cfa4a4c9dec7e67817ad618d57d387a19e7 Mon Sep 17 00:00:00 2001 From: dinex-dev Date: Wed, 29 Jul 2026 10:44:31 +0530 Subject: [PATCH 1/3] fix(auth): prevent open redirect after login (RQ-2303, CWE-601) LoginHandler.redirect() opened any cross-origin URL via window.open, allowing an attacker-supplied redirectURL/redirectURI to send an authenticated user to an external phishing page after login. Route non-same-origin targets to redirectToHome instead of opening them. Same-origin redirects are unaffected and still preserve the user's original request path. Co-Authored-By: Claude Opus 4.8 --- app/src/components/authentication/LoginHandler/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/components/authentication/LoginHandler/index.tsx b/app/src/components/authentication/LoginHandler/index.tsx index 9ad137cafb..f06d08582c 100644 --- a/app/src/components/authentication/LoginHandler/index.tsx +++ b/app/src/components/authentication/LoginHandler/index.tsx @@ -69,7 +69,8 @@ const LoginHandler: React.FC = () => { const navigateParams = urlObj.pathname + urlObj.search; navigate(navigateParams); } else { - window.open(url, "_self"); + // Never redirect to a different origin (open-redirect / CWE-601). + redirectToHome(appMode, navigate); } } catch (error) { Logger.log("[LoginHandler-redirect] catch", { error }); From cf736d618acc7ce248591bfa48b7d155b1a1b751 Mon Sep 17 00:00:00 2001 From: dinex-dev Date: Wed, 29 Jul 2026 11:03:06 +0530 Subject: [PATCH 2/3] fix(security): remove rehype-raw to prevent stored XSS (RQ-2304, CWE-79) ReactMarkdown was configured with rehypeRaw, which re-parses raw HTML in Markdown source into live DOM nodes. In the API Client collection description (user-editable, persisted to Firebase, shown to teammates) this allowed stored XSS via payloads like . Drop rehypeRaw from CollectionOverview and BaseBanner so ReactMarkdown falls back to its safe default (raw HTML escaped). Standard Markdown via remark-gfm is unaffected. Co-Authored-By: Claude Opus 4.8 --- .../AppNotificationBanner/components/BaseBanner.tsx | 3 +-- .../components/CollectionOverview/CollectionOverview.tsx | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/componentsV2/AppNotificationBanner/components/BaseBanner.tsx b/app/src/componentsV2/AppNotificationBanner/components/BaseBanner.tsx index 74bf25bb81..de0335ecda 100644 --- a/app/src/componentsV2/AppNotificationBanner/components/BaseBanner.tsx +++ b/app/src/componentsV2/AppNotificationBanner/components/BaseBanner.tsx @@ -2,7 +2,6 @@ import React from "react"; import { RQButton } from "lib/design-system/components"; import ReactMarkdown from "react-markdown"; import { Banner, BANNER_TYPE } from "../banner.types"; -import rehypeRaw from "rehype-raw"; import { capitalize } from "lodash"; import { trackAppBannerCtaClicked } from "../analytics"; import { RequestBillingTeamAccessModal } from "features/settings"; @@ -49,7 +48,7 @@ export const BaseBanner: React.FC = ({ )}
- {text} + {text}
{actionsConfig && actionsConfig.length > 0 && ( diff --git a/app/src/features/apiClient/screens/apiClient/components/views/components/Collection/components/CollectionOverview/CollectionOverview.tsx b/app/src/features/apiClient/screens/apiClient/components/views/components/Collection/components/CollectionOverview/CollectionOverview.tsx index 48b7614869..53191a0ea7 100644 --- a/app/src/features/apiClient/screens/apiClient/components/views/components/Collection/components/CollectionOverview/CollectionOverview.tsx +++ b/app/src/features/apiClient/screens/apiClient/components/views/components/Collection/components/CollectionOverview/CollectionOverview.tsx @@ -4,7 +4,6 @@ import * as Sentry from "@sentry/react"; import { InlineInput } from "componentsV2/InlineInput/InlineInput"; import { Input, notification, Tabs } from "antd"; import ReactMarkdown from "react-markdown"; -import rehypeRaw from "rehype-raw"; import remarkGfm from "remark-gfm"; import { useOutsideClick } from "hooks"; import { useRBAC } from "features/rbac"; @@ -107,7 +106,6 @@ export const CollectionOverview: React.FC = ({ collecti return ( Date: Wed, 29 Jul 2026 11:58:29 +0530 Subject: [PATCH 3/3] fix(security): validate postMessage origin in rule editor (RQ-2309, CWE-79) The rule editor's message listener accepted any message whose body contained author:"requestly"/action:"ruleEditor:loadData" with no event.origin check, so any web page could open the editor in a popup and inject attacker-supplied ruleData (e.g. a malicious Script rule). Reject messages unless event.origin is the app's own origin or a Requestly extension origin (chrome/moz/safari-web-extension scheme). This preserves the intended DevTools "create rule from traffic" flow (a chrome-extension:// sender) while blocking arbitrary web origins. The outbound ready-event postMessage still uses "*", but only leaks a blank default rule template in CREATE mode; tightening its target origin needs a coordinated extension change and is left as follow-up. Co-Authored-By: Claude Opus 4.8 --- .../RuleBuilder/useExternalRuleCreation.ts | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/app/src/components/features/rules/RuleBuilder/useExternalRuleCreation.ts b/app/src/components/features/rules/RuleBuilder/useExternalRuleCreation.ts index 1632e91f45..1d2a37f4f2 100644 --- a/app/src/components/features/rules/RuleBuilder/useExternalRuleCreation.ts +++ b/app/src/components/features/rules/RuleBuilder/useExternalRuleCreation.ts @@ -8,6 +8,27 @@ import { Rule } from "@requestly/shared/types/entities/rules"; const { RULE_EDITOR_CONFIG } = APP_CONSTANTS; const REQUESTLY_POST_MESSAGE_AUTHOR = "requestly"; +// Browser-extension schemes for the only legitimate external caller of this +// channel: the Requestly extension's DevTools "create rule from traffic" flow. +// (chrome-extension for Chrome/Edge, moz-extension for Firefox, safari-web-extension for Safari.) +const TRUSTED_EXTENSION_ORIGIN_PROTOCOLS = ["chrome-extension:", "moz-extension:", "safari-web-extension:"]; + +/** + * Guards the rule-editor postMessage listener against cross-origin injection + * (CWE-79 / RQ-2309). Only the app's own origin and the Requestly extension are + * trusted senders; messages from arbitrary web pages are rejected. + */ +const isTrustedRuleEditorMessageOrigin = (origin: string): boolean => { + if (origin === window.location.origin) { + return true; + } + try { + return TRUSTED_EXTENSION_ORIGIN_PROTOCOLS.includes(new URL(origin).protocol); + } catch { + return false; + } +}; + interface PostMessageData { author: string; action: string; @@ -26,7 +47,10 @@ const useExternalRuleCreation = (mode: string): void => { useEffect(() => { const onMessageReceived = (event: MessageEvent) => { - const { author, action, payload } = event.data; + if (!isTrustedRuleEditorMessageOrigin(event.origin)) { + return; + } + const { author, action, payload } = event.data ?? {}; if (author === REQUESTLY_POST_MESSAGE_AUTHOR && action === "ruleEditor:loadData") { const { ruleData, inputSelectorToFocus } = payload; setCurrentlySelectedRule(dispatch, ruleData); @@ -57,6 +81,12 @@ const useExternalRuleCreation = (mode: string): void => { ruleData: currentlySelectedRuleData, }, }, + // Accepted risk (RQ-2309): wildcard target origin. Safe here because this only + // fires in CREATE mode where ruleData is a blank default template (no user data), + // and the injection vector is already closed by the inbound origin check above. + // A concrete target origin can't be used without a coordinated extension change, + // since the legitimate opener is the extension (chrome/moz/safari-web-extension://, + // which varies by browser and build). Do not broadcast sensitive data through here. "*" ); hasSentReadyEventRef.current = true;