feat: Add ChatGPT App sample with MoEngage integration#40
feat: Add ChatGPT App sample with MoEngage integration#40Aman-engg-sdk wants to merge 6 commits into
Conversation
- Next.js 14 app with TypeScript - Full MoEngage WebSDK integration - ChatGPT Apps SDK integration - AI bot detection and blocking - AI-assisted browser detection - Comprehensive event tracking - User identification and session management - Complete documentation and testing guides
- .next is a build artifact and should only exist inside chatgpt-app-sample when building - Already properly gitignored in chatgpt-app-sample/.gitignore
- All project files should be contained within chatgpt-app-sample directory - next-env.d.ts is Next.js TypeScript definitions file - vercel.json is deployment configuration
| if (typeof window === 'undefined') return false; | ||
|
|
||
| const isGPTApp = | ||
| window.location.href.includes('chat.openai.com') || |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
How to fix the problem in general:
Instead of using a substring check on the entire URL, parse the URL to extract the hostname and compare it against a whitelist of allowed hostnames. This prevents false positives from matches in the query string, path, or other portions of the URL.
Detailed best fix for the shown code:
- Use the browser's built-in
URLclass to parsewindow.location.hrefand (for parent frame)window.parent.location.href, then inspect their.hostnameproperty. - Compare against an explicit allowlist of hostnames, e.g.,
['chat.openai.com', 'chatgpt.com']. - Apply this same logic for both the current window and parent frame checks.
- You can use
hostname.endsWith(...)if you want to support subdomains too (cautiously), but to faithfully match the original intention, strict equality is appropriate.
Where to apply changes:
- Lines 124, 125: Replace substring checks on
window.location.href. - Lines 128-129: Similarly, replace substring checks on
window.parent.location.href.
Imports/definitions needed:
- Use the native
URLobject (well supported on all modern browsers, no import needed).
| @@ -120,13 +120,24 @@ | ||
| detectGPTApp(): boolean { | ||
| if (typeof window === 'undefined') return false; | ||
|
|
||
| const allowedHostnames = ['chat.openai.com', 'chatgpt.com']; | ||
| let currentHostname: string, parentHostname: string | null = null; | ||
| try { | ||
| currentHostname = new URL(window.location.href).hostname; | ||
| } catch { | ||
| currentHostname = ''; | ||
| } | ||
| if (window.parent !== window) { | ||
| try { | ||
| parentHostname = new URL(window.parent.location.href).hostname; | ||
| } catch { | ||
| parentHostname = null; | ||
| } | ||
| } | ||
| const isGPTApp = | ||
| window.location.href.includes('chat.openai.com') || | ||
| window.location.href.includes('chatgpt.com') || | ||
| (window as any).__OPENAI_APPS__ !== undefined || | ||
| (window.parent !== window && | ||
| (window.parent.location.href.includes('chat.openai.com') || | ||
| window.parent.location.href.includes('chatgpt.com'))); | ||
| allowedHostnames.includes(currentHostname) || | ||
| ((window as any).__OPENAI_APPS__ !== undefined) || | ||
| (parentHostname !== null && allowedHostnames.includes(parentHostname)); | ||
|
|
||
| this.detectionResults.isGPTApp = isGPTApp; | ||
| this.detectionResults.detectionDetails.gptAppDetection = { |
|
|
||
| const isGPTApp = | ||
| window.location.href.includes('chat.openai.com') || | ||
| window.location.href.includes('chatgpt.com') || |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
General fix:
Instead of performing a substring check on the full URL (which is vulnerable to bypasses and false positives), parse the current page's URL and match the hostname property against an explicit allowlist containing 'chat.openai.com', 'chatgpt.com', and their canonical subdomains. This also applies to the iframe parent domain check.
Detailed steps to fix:
- Parse the current location URL (and
window.parent.location.hrefif checking the parent) using the built-inURLclass. - Safely compare the
hostnameproperty to an allow-list, e.g.,['chat.openai.com', 'chatgpt.com', 'www.chatgpt.com']. - Do not use
.includes()on the entire URL string. - Ensure to handle exceptions in case the URL constructor fails (e.g., malformed URL in a hostile iframe), or the host is unavailable due to cross-origin restrictions.
Code Regions to Change:
- Replace the substring checks on lines 124 and 125 with explicit checks of the parsed hostname.
- Similarly, update the check in the iframe branch (lines 127–129) to parse and check the parent's location hostname safely.
Implementation considerations:
- Import or define an allow-list of valid GPT app hostnames.
- Account for
window.parent.locationpossibly throwing if cross-origin (wrap in try/catch). - No new dependencies needed, only standard JavaScript API.
| @@ -120,13 +120,27 @@ | ||
| detectGPTApp(): boolean { | ||
| if (typeof window === 'undefined') return false; | ||
|
|
||
| const allowedHosts = ['chat.openai.com', 'chatgpt.com', 'www.chatgpt.com']; | ||
| function isAllowedHost(urlStr: string | null): boolean { | ||
| if (!urlStr) return false; | ||
| try { | ||
| const host = new URL(urlStr).hostname; | ||
| return allowedHosts.includes(host); | ||
| } catch (e) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| const isGPTApp = | ||
| window.location.href.includes('chat.openai.com') || | ||
| window.location.href.includes('chatgpt.com') || | ||
| isAllowedHost(window.location.href) || | ||
| (window as any).__OPENAI_APPS__ !== undefined || | ||
| (window.parent !== window && | ||
| (window.parent.location.href.includes('chat.openai.com') || | ||
| window.parent.location.href.includes('chatgpt.com'))); | ||
| (window.parent !== window && (() => { | ||
| try { | ||
| return isAllowedHost(window.parent.location.href); | ||
| } catch (e) { | ||
| return false; | ||
| } | ||
| })()); | ||
|
|
||
| this.detectionResults.isGPTApp = isGPTApp; | ||
| this.detectionResults.detectionDetails.gptAppDetection = { |
| window.location.href.includes('chatgpt.com') || | ||
| (window as any).__OPENAI_APPS__ !== undefined || | ||
| (window.parent !== window && | ||
| (window.parent.location.href.includes('chat.openai.com') || |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
The bug: The code currently checks for the presence of "chat.openai.com" and "chatgpt.com" as substrings anywhere in window.location.href and (if in an iframe) window.parent.location.href. Instead, the hostname part of the URL should be parsed and compared strictly against allowed hostnames or their subdomains.
How to fix:
- Replace all uses of
.includes('chat.openai.com')and.includes('chatgpt.com')with proper hostname checks using the URL API (new URL(...)). Extract thehostnameand check if it matches the allowed names. - To avoid breaking sites served from subdomains (like
beta.chat.openai.com), allow either exact matches or specific subdomains (optionally using a helper function). - For iframe detection, wrap any
window.parent.location.hrefusage in a try/catch to handle cross-origin errors. - Only change the
detectGPTApp()method as shown.
What is needed:
- Use the standard
URLclass for parsing. - Add a helper function to validate hostnames (either exact match or subdomain).
- Wrap access to
window.parent.location.hrefwith try/catch.
| @@ -120,19 +120,46 @@ | ||
| detectGPTApp(): boolean { | ||
| if (typeof window === 'undefined') return false; | ||
|
|
||
| const isGPTApp = | ||
| window.location.href.includes('chat.openai.com') || | ||
| window.location.href.includes('chatgpt.com') || | ||
| (window as any).__OPENAI_APPS__ !== undefined || | ||
| (window.parent !== window && | ||
| (window.parent.location.href.includes('chat.openai.com') || | ||
| window.parent.location.href.includes('chatgpt.com'))); | ||
| function isAllowedHost(hostname: string): boolean { | ||
| const allowed = ['chat.openai.com', 'chatgpt.com']; | ||
| // Check for exact match, or direct subdomains of these hosts | ||
| return allowed.some( | ||
| (h) => hostname === h || hostname.endsWith('.' + h) | ||
| ); | ||
| } | ||
|
|
||
| let isGPTApp = false; | ||
| try { | ||
| const pageHost = new URL(window.location.href).hostname; | ||
| isGPTApp = | ||
| isAllowedHost(pageHost) || | ||
| (window as any).__OPENAI_APPS__ !== undefined; | ||
| } catch (e) { | ||
| // Ignore invalid URL | ||
| } | ||
|
|
||
| // Check parent frame if exists and not the same window | ||
| let inIframe = false; | ||
| let parentUrl: string | null = null; | ||
| if (window.parent !== window) { | ||
| inIframe = true; | ||
| try { | ||
| const purl = window.parent.location.href; | ||
| parentUrl = purl; | ||
| const parentHost = new URL(purl).hostname; | ||
| if (isAllowedHost(parentHost)) { | ||
| isGPTApp = true; | ||
| } | ||
| } catch (e) { | ||
| // Can't access parent URL due to cross-origin; leave parentUrl as null | ||
| } | ||
| } | ||
|
|
||
| this.detectionResults.isGPTApp = isGPTApp; | ||
| this.detectionResults.detectionDetails.gptAppDetection = { | ||
| detected: isGPTApp, | ||
| inIframe: window.parent !== window, | ||
| parentUrl: window.parent !== window ? window.parent.location.href : null, | ||
| inIframe: inIframe, | ||
| parentUrl: parentUrl, | ||
| }; | ||
|
|
||
| return isGPTApp; |
| (window as any).__OPENAI_APPS__ !== undefined || | ||
| (window.parent !== window && | ||
| (window.parent.location.href.includes('chat.openai.com') || | ||
| window.parent.location.href.includes('chatgpt.com'))); |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To securely detect whether the current or parent window is hosted on "chat.openai.com" or "chatgpt.com", the code should parse the relevant URL and examine the hostname directly. This way, it only matches exact allowed hostnames (like 'chat.openai.com' or 'chatgpt.com') and not any arbitrary host or embedded path/query. The best fix is to define a list of allowed hosts, parse the hostname from window.location.href and window.parent.location.href, and check for exact matches (or, if appropriate, check if the hostname ends with ".chatgpt.com" or similar, after validating for subdomains).
Changes needed:
- At the start of
detectGPTApp, define a whitelist of allowed hostnames, e.g.['chat.openai.com', 'chatgpt.com']. - For both the current window and its parent (if not same as self), parse the URLs using the standard
URLconstructor and check if.hostnameequals one of the allowed hosts. - Fallback to the previous check for the special case of
__OPENAI_APPS__as in current logic.
Requires:
- Import or usage of
URL(native global constructor in modern browsers). - Updating all substring matches in
detectGPTApp(within.href.includes('...')) to correctly parse and check hostname instead.
No external dependencies are needed; the standard URL class suffices.
| @@ -120,13 +120,21 @@ | ||
| detectGPTApp(): boolean { | ||
| if (typeof window === 'undefined') return false; | ||
|
|
||
| const allowedHosts = ['chat.openai.com', 'chatgpt.com']; | ||
| let currentHost = ''; | ||
| let parentHost = ''; | ||
| try { | ||
| currentHost = new URL(window.location.href).hostname; | ||
| } catch {} | ||
| if (window.parent !== window) { | ||
| try { | ||
| parentHost = new URL(window.parent.location.href).hostname; | ||
| } catch {} | ||
| } | ||
| const isGPTApp = | ||
| window.location.href.includes('chat.openai.com') || | ||
| window.location.href.includes('chatgpt.com') || | ||
| (window as any).__OPENAI_APPS__ !== undefined || | ||
| (window.parent !== window && | ||
| (window.parent.location.href.includes('chat.openai.com') || | ||
| window.parent.location.href.includes('chatgpt.com'))); | ||
| allowedHosts.includes(currentHost) || | ||
| ((window as any).__OPENAI_APPS__ !== undefined) || | ||
| (window.parent !== window && allowedHosts.includes(parentHost)); | ||
|
|
||
| this.detectionResults.isGPTApp = isGPTApp; | ||
| this.detectionResults.detectionDetails.gptAppDetection = { |
| if (typeof window === 'undefined') return false; | ||
|
|
||
| return ( | ||
| window.location.href.includes('chat.openai.com') || |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To correctly check if the current page or its parent is running on an allowed host (either chat.openai.com, chatgpt.com, or an explicitly whitelisted subdomain thereof), we should:
- Parse the URL using the
URLconstructor to extract thehostnamefield, which is guaranteed to be the authority portion (host) of the URL and not contain parts of the pathname or query. - Check the hostname against an explicit whitelist:
chat.openai.com,www.chatgpt.com,chatgpt.com, and any other valid subdomains, as appropriate. - For extensibility:
- It is usually safer to require an exact match or a subdomain match (e.g.,
something.chat.openai.combut notchat.openai.com.evil.com). This is achieved by checking for equality or forendsWith('.allowed.com').
- It is usually safer to require an exact match or a subdomain match (e.g.,
Implementation:
- In the two places in
isChatGPTAppwherewindow.location.href.includes(...)is used, replace with checked.hostnameusing aURLobject. - Do the same for
window.parent.location.href, but wrap in a try/catch since cross-origin access to window.parent.location may throw. - Define a whitelist array (e.g.,
const allowedHosts). - Replace all substring checks with exact/endsWith checks on the hostname.
No new dependencies or uncommon APIs are needed (URL is native).
| @@ -33,13 +33,35 @@ | ||
| export const isChatGPTApp = (): boolean => { | ||
| if (typeof window === 'undefined') return false; | ||
|
|
||
| const allowedHosts = [ | ||
| 'chat.openai.com', | ||
| 'chatgpt.com', | ||
| 'www.chatgpt.com' | ||
| ]; | ||
|
|
||
| // Helper to check if a given location belongs to an allowed host | ||
| const isAllowedHost = (loc: Location): boolean => { | ||
| try { | ||
| const { hostname } = new URL(loc.href); | ||
| return allowedHosts.includes(hostname); | ||
| } catch { | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| window.location.href.includes('chat.openai.com') || | ||
| window.location.href.includes('chatgpt.com') || | ||
| isAllowedHost(window.location) || | ||
| window.__OPENAI_APPS__ !== undefined || | ||
| (window.parent !== window && | ||
| (window.parent.location.href.includes('chat.openai.com') || | ||
| window.parent.location.href.includes('chatgpt.com'))) | ||
| // Cross-origin access to window.parent.location may throw, so wrap in try/catch. | ||
| (() => { | ||
| try { | ||
| return isAllowedHost(window.parent.location); | ||
| } catch { | ||
| return false; | ||
| } | ||
| })() | ||
| ) | ||
| ); | ||
| }; | ||
|
|
|
|
||
| return ( | ||
| window.location.href.includes('chat.openai.com') || | ||
| window.location.href.includes('chatgpt.com') || |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To fix the problem, we must parse the URL using the appropriate API and check the host or hostname property rather than performing a substring check on the full URL. We should replace all instances of window.location.href.includes('chatgpt.com') (and similarly for chat.openai.com) with a check that parses the host part of the URL and matches it against an allowlist of acceptable hostnames, e.g., 'chat.openai.com' and 'chatgpt.com' (and their canonical subdomains, if needed).
We'll use the built-in URL class (available in modern browsers and likely for this code) to parse window.location.href and window.parent.location.href.
Change applicable at lines 36-43 in chatgpt-app-sample/lib/chatgpt-apps.ts:
- Create an allowlist:
['chat.openai.com', 'chatgpt.com'] - Get
window.location.hostnameandwindow.parent.location.hostname - Replace the
includes()checks with an array.includes()match against the parsed hostname.
No additional package imports are needed. The built-in URL object suffices.
| @@ -33,13 +33,22 @@ | ||
| export const isChatGPTApp = (): boolean => { | ||
| if (typeof window === 'undefined') return false; | ||
|
|
||
| return ( | ||
| window.location.href.includes('chat.openai.com') || | ||
| window.location.href.includes('chatgpt.com') || | ||
| window.__OPENAI_APPS__ !== undefined || | ||
| (window.parent !== window && | ||
| (window.parent.location.href.includes('chat.openai.com') || | ||
| window.parent.location.href.includes('chatgpt.com'))) | ||
| ); | ||
| const allowedHosts = ['chat.openai.com', 'chatgpt.com']; | ||
|
|
||
| let hostMatch = | ||
| allowedHosts.includes(window.location.hostname) || | ||
| window.__OPENAI_APPS__ !== undefined; | ||
|
|
||
| // Check parent window if it's not the same as current window (cross-origin may throw error) | ||
| if (!hostMatch && window.parent !== window) { | ||
| try { | ||
| hostMatch = | ||
| allowedHosts.includes(window.parent.location.hostname); | ||
| } catch (e) { | ||
| // Ignore cross-origin access errors | ||
| } | ||
| } | ||
|
|
||
| return hostMatch; | ||
| }; | ||
|
|
| window.location.href.includes('chatgpt.com') || | ||
| window.__OPENAI_APPS__ !== undefined || | ||
| (window.parent !== window && | ||
| (window.parent.location.href.includes('chat.openai.com') || |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
The best way to fix this problem is to parse the URL using the browser's URL constructor, then check the host (or hostname) field against an explicit whitelist of allowed hosts. This avoids substring checks and ensures only exact matches to desired hosts or subdomains. For each place that currently uses .includes('chat.openai.com') or .includes('chatgpt.com') on window.location.href (or parent), replace it with a parsed check against the host, e.g.:
const allowedHosts = ['chat.openai.com', 'chatgpt.com'];
allowedHosts.includes(new URL(window.location.href).host)or, if you want to include subdomains, resolve how broad you want the match to be (possibly using .endsWith(...)). Update all four relevant checks (self/parent and both host names).
If you use the URL constructor, no imports are required. If the URL is unparseable, you may want to handle exceptions gracefully.
The changes should stay within the context of function isChatGPTApp, replacing lines where .includes('chat.openai.com') or .includes('chatgpt.com') are called on URLs.
| @@ -33,13 +33,23 @@ | ||
| export const isChatGPTApp = (): boolean => { | ||
| if (typeof window === 'undefined') return false; | ||
|
|
||
| return ( | ||
| window.location.href.includes('chat.openai.com') || | ||
| window.location.href.includes('chatgpt.com') || | ||
| window.__OPENAI_APPS__ !== undefined || | ||
| (window.parent !== window && | ||
| (window.parent.location.href.includes('chat.openai.com') || | ||
| window.parent.location.href.includes('chatgpt.com'))) | ||
| ); | ||
| try { | ||
| const allowedHosts = ['chat.openai.com', 'chatgpt.com']; | ||
| const currentHost = new URL(window.location.href).host; | ||
| if (allowedHosts.includes(currentHost)) return true; | ||
| if (window.__OPENAI_APPS__ !== undefined) return true; | ||
| if (window.parent !== window) { | ||
| let parentHost: string; | ||
| try { | ||
| parentHost = new URL(window.parent.location.href).host; | ||
| } catch (e) { | ||
| parentHost = ''; | ||
| } | ||
| if (allowedHosts.includes(parentHost)) return true; | ||
| } | ||
| } catch (e) { | ||
| // Swallow parsing exceptions | ||
| } | ||
| return false; | ||
| }; | ||
|
|
| window.__OPENAI_APPS__ !== undefined || | ||
| (window.parent !== window && | ||
| (window.parent.location.href.includes('chat.openai.com') || | ||
| window.parent.location.href.includes('chatgpt.com'))) |
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 5 months ago
To address the URL substring check, the function should reliably determine the hostname portion of the location object (rather than scanning for a substring in the whole URL), and compare it to a fixed allow-list of trusted hosts—for example, only chat.openai.com and chatgpt.com (and, optionally, their subdomains if required).
- Extract the hostname using the
location.hostnameproperty. - Directly compare
hostnameagainst a whitelist (e.g.,'chat.openai.com','chatgpt.com'). - Do this for both
window.locationand, if checked,window.parent.location. - No additional dependencies are required (standard DOM
locationis sufficient). - Only make the change within the affected (shown) code, so edit lines 37-42 so that the domain checks use
location.hostname.
| @@ -33,13 +33,12 @@ | ||
| export const isChatGPTApp = (): boolean => { | ||
| if (typeof window === 'undefined') return false; | ||
|
|
||
| const allowedHosts = ['chat.openai.com', 'chatgpt.com']; | ||
| return ( | ||
| window.location.href.includes('chat.openai.com') || | ||
| window.location.href.includes('chatgpt.com') || | ||
| allowedHosts.includes(window.location.hostname) || | ||
| window.__OPENAI_APPS__ !== undefined || | ||
| (window.parent !== window && | ||
| (window.parent.location.href.includes('chat.openai.com') || | ||
| window.parent.location.href.includes('chatgpt.com'))) | ||
| (allowedHosts.includes(window.parent.location.hostname))) | ||
| ); | ||
| }; | ||
|
|
Overview
This PR adds a new sample application demonstrating MoEngage WebSDK integration with ChatGPT Apps.
Features
What's Included
chatgpt-app-sample/folderTesting
npm install && npm run devDocumentation
All documentation is included in the
chatgpt-app-sample/folder. See README.md for quick start.