-
-
Save charl-kruger/24ec3b26d28c6e9f9e3084acf3780050 to your computer and use it in GitHub Desktop.
| ## Verdict | |
| **I would not approve the standard `surfly.js` integration for authenticated or transactional banking pages as-is.** It is not just a “support widget”; it is a third-party JavaScript loader plus a co-browsing/proxy system that can interact with authenticated browser state, page contents, navigation, events, files, screenshots, and session control. That threat model is too powerful for core banking unless it is heavily isolated, hardened, independently reviewed, and preferably deployed in a controlled/private/on-prem model. | |
| One important limitation: I could not retrieve the raw live `https://surfly.com/surfly.js` body through the available web reader because it rejected the JavaScript content type, and the execution container could not resolve the host. So this is **not** a line-by-line audit of the minified live asset. For a banking approval, that alone is a gating issue: you should require the exact source, sourcemaps where applicable, a version-pinned artifact, dependency inventory, hashes, and change-control evidence before go-live. The public docs and integration model are still enough to conclude that the default/public SaaS integration is not suitable for sensitive banking flows. | |
| --- | |
| ## What Surfly is actually doing | |
| The documented integration dynamically injects a remote script from `https://surfly.com/surfly.js` into the page, asynchronously loads the Surfly libraries, and then exposes `Surfly.init()` and other API methods to the host page. The current quickstart explicitly shows a loader that creates a `<script>` element, sets `async = 1`, and sets `src` to `https://surfly.com/surfly.js`; it also notes that the rest of the API is not available until initialization is ready. ([docs.surfly.com][1]) | |
| Architecturally, Surfly describes itself as JavaScript plus a **content-rewriting proxy**. In a session, the leader browser’s request goes to the Surfly proxy, the proxy modifies the request to look as if it came from `surfly.com`, sends it to the original site, receives the response, modifies that content so it can load inside an iframe, and synchronizes interaction between leader and follower browsers. Surfly’s own docs say the proxy “overcomes cross-domain policies” and allows co-browsing of logged-in sessions while not sending security tokens/passwords to the follower. ([docs.surfly.com][2]) | |
| That is a powerful model. It means a banking site would be adding a third-party runtime and proxy-based browsing layer into the same customer journey where balances, account numbers, payment forms, identity flows, tokens, statements, and fraud controls may exist. | |
| --- | |
| ## Main security conclusion | |
| **This is not secure enough for a banking platform in its default or typical documented form.** | |
| It may be acceptable only for a constrained use case such as: | |
| **“Agent can view and guide a user on a non-transactional support surface, with sensitive fields masked, no agent control, no cookie/storage transfer, no file sharing, no screenshots/recording, no arbitrary navigation, strict allowlisting, server-side transaction blocking, version-pinned code, and independent audit of the actual Surfly JavaScript and proxy.”** | |
| For anything involving authenticated balances, statements, onboarding KYC, card data, payments, beneficiary creation, loan applications, password reset, OTP entry, PIN entry, or privileged admin actions, I would treat it as **not approved unless isolated into a specially designed support environment**. | |
| --- | |
| ## Critical findings | |
| ### 1. Third-party JavaScript supply-chain risk is fundamental here | |
| The bank page loads executable JavaScript from Surfly’s domain. The documented snippet does not show Subresource Integrity, an immutable versioned asset URL, or a pinned hash; it dynamically loads `https://surfly.com/surfly.js`. ([docs.surfly.com][1]) | |
| That matters because third-party JavaScript executes with the privileges of the embedding page. OWASP’s third-party JavaScript guidance describes this class of risk as effectively comparable to XSS: the script can execute in the user’s browser context, and key risks include arbitrary code execution, loss of control over upstream changes, and sensitive-data leakage. ([OWASP Cheat Sheet Series][3]) | |
| For a bank, a compromised or maliciously changed `surfly.js` could potentially: | |
| * Read or alter DOM content, including balances, names, account details, form fields, and transaction screens. | |
| * Trigger same-origin requests to banking APIs from the customer’s browser; HttpOnly cookies would still be attached by the browser even if JavaScript cannot read them directly. | |
| * Manipulate payment or beneficiary forms before submission. | |
| * Capture interaction telemetry, form values, navigation state, or screenshots depending on configuration and runtime behavior. | |
| * Bypass the bank’s own release/change-control process because code is being pulled from a third-party origin at runtime. | |
| This is not an allegation that Surfly is malicious. It is an inherent risk of placing mutable third-party JavaScript inside authenticated banking pages. | |
| --- | |
| ### 2. The proxy model is a deliberate man-in-the-browser / man-in-the-session pattern | |
| Surfly’s technology documentation says the user’s web requests are routed through a Surfly proxy, that the proxy rewrites responses so they can be loaded in an iframe, and that this mechanism is designed to overcome cross-domain restrictions. ([docs.surfly.com][2]) | |
| That is exactly the kind of mechanism banks usually design controls to prevent. Banking security often depends on origin boundaries, device/session binding, anti-clickjacking, CSP, secure cookies, SameSite behavior, fraud telemetry, bot detection, step-up authentication, and transaction-signing semantics. A proxy that rewrites content and synchronizes browser activity can interfere with or complicate those controls. | |
| Surfly says security tokens and passwords are not sent to followers, and that followers see only visual changes. ([docs.surfly.com][2]) That is useful, but it is not enough for banking approval. The bank would still need to prove exactly where sensitive data is visible, where it is processed, whether it is transformed before or after leaving the customer browser, what the proxy can see, what is logged, what metadata is stored, and how session state is handled. | |
| --- | |
| ### 3. Cookie, storage, and form transfer defaults are dangerous for banking | |
| Surfly’s session options document shows `cookie_transfer_enabled` defaults to `true`, and describes it as transferring cookies, localStorage, sessionStorage, and form fields into the session and back, excluding form fields on the back-transfer. It also documents `cookie_transfer_proxying`, which can transfer HttpOnly cookies between the session and original page when configured with a continuation point. ([docs.surfly.com][4]) | |
| For banking, this is a major red flag. | |
| Bank authentication state is often stored across Secure/HttpOnly/SameSite cookies, localStorage, sessionStorage, browser-bound tokens, anti-CSRF state, device-binding values, and fraud-analysis identifiers. A co-browsing product that can transfer or proxy these values can undermine assumptions made by the bank’s session-management design. | |
| At minimum, a banking configuration should disable cookie/storage transfer unless there is a very carefully reviewed, narrow, non-authenticated support use case. I would consider these settings unacceptable on core banking pages: | |
| ```js | |
| cookie_transfer_enabled: true | |
| cookie_transfer_proxying: true | |
| enable_cookie_backtransfer: true | |
| ``` | |
| The safe posture for banking is to avoid transferring authentication state into a co-browsing context at all. | |
| --- | |
| ### 4. Masking is useful, but fragile and not sufficient | |
| Surfly documents field masking and says password fields are masked by default, while other fields require either the `surfly_private` attribute or configuration such as `hide_element_by_selector`. It also documents an important limitation: masked fields inside `GET` forms, or masked values that become part of URLs, may still be trackable through audit logs and browser network activity. ([docs.surfly.com][5]) | |
| That limitation is highly relevant for banking. Sensitive values often appear not just in visible fields but also in: | |
| * URLs and query parameters. | |
| * Hidden fields. | |
| * DOM attributes. | |
| * Client-side state stores. | |
| * JavaScript variables. | |
| * Analytics events. | |
| * Error messages. | |
| * Download URLs. | |
| * Statement/document viewers. | |
| * Shadow DOM or dynamically rendered components. | |
| * Autocomplete and browser-managed inputs. | |
| Masking is an implementation detail, not a security boundary. A bank should not rely on CSS selectors or field attributes as the primary control protecting account numbers, card numbers, balances, OTPs, PINs, KYC information, payment details, or statement data. | |
| --- | |
| ### 5. Default session-control options are too permissive | |
| The documented defaults include several settings that are risky for a bank. Surfly’s options show `host_switching_allowed: true`, `participants_can_request_to_interact: true`, `new_urls_allowed: true`, `non_hosts_can_open_tabs: true`, and `allow_opening_urls_from_query_parameter: true`. The same options list shows `private_session: false`, `password_required: false`, and `admission_enabled: false` by default. ([docs.surfly.com][4]) | |
| Surfly’s session API also exposes leader/follower links, PINs, host switching, tab control, control transfer, URL relocation, file upload, screenshots, and broadcast messaging. ([docs.surfly.com][6]) | |
| For banking, these defaults are not acceptable. A leaked follower link, weak join process, permissive control transfer, arbitrary navigation, or accidental screen-share into sensitive pages could expose regulated data. Even if Surfly has server-side protections, the integration should start from a denial posture: no arbitrary navigation, no default invitations, no agent control, no participant control request, no host switching, no file upload, no screenshot, and no recording unless there is a specific approved need. | |
| --- | |
| ### 6. The messaging bridge can become a cross-context attack surface | |
| Surfly documents `sendMessage(message, targetOrigin)` for communication between the original page and proxified version, and explicitly notes that using `targetOrigin = "*"` delivers the message regardless of recipient origin. ([docs.surfly.com][6]) | |
| That creates an integration risk. If the bank’s application, Surfly widget, or session extensions use permissive message handling, this can become a cross-origin bridge problem: sensitive data or commands may be sent to the wrong context, or untrusted contexts may influence the banking page. | |
| For a bank, any `postMessage`-style integration must use exact origin checks, strict schema validation, replay protection where needed, and no wildcard target origins for sensitive messages. | |
| --- | |
| ### 7. Audit logs, screenshots, file sharing, chat, and events can leak sensitive data | |
| Surfly documents audit logging for actions such as button clicks, text inputs, control transfers, documents shared, and pages visited. The docs also list events containing page URLs, form changes, submissions, clicks, participant metadata, IP addresses, names, and control transfers. ([docs.surfly.com][5]) | |
| The JS API also exposes events such as user activity, relocation, tab control, file download, messages, and session lifecycle events. Some events include URLs, filenames, user data, origins, and final locations. ([docs.surfly.com][7]) | |
| For normal customer support, this may be useful. For banking, it can become a regulated-data leakage path. Page URLs can contain identifiers. File names can reveal statements or claims. Form-change logs can accidentally capture customer input. Screenshots can capture balances, statements, KYC documents, or card details. Chat logs can contain PII or authentication information. | |
| Surfly does provide controls for audit logs, screenshots, video, screen sharing, file sharing, and privacy options, but the existence of these capabilities means the bank needs a strict data-retention and data-minimization design rather than a simple widget integration. ([docs.surfly.com][4]) | |
| --- | |
| ### 8. Client-side “disable submit button” controls are not enough | |
| Surfly’s security documentation includes an example approach for disabling submit buttons while an agent has control. ([docs.surfly.com][5]) | |
| That may reduce accidental agent actions, but it is not a banking-grade control. Any control implemented only in browser JavaScript can be bypassed by bugs, DOM changes, race conditions, malicious scripts, browser devtools, API calls, or integration mistakes. | |
| For banking, transactional protection must be enforced server-side. Examples: | |
| * Agents must never be able to initiate or approve payments. | |
| * Agent-controlled sessions must be blocked from payment, beneficiary, card-management, loan-approval, password-reset, OTP/PIN, or statement-download endpoints. | |
| * “Customer in control” must be proven server-side, not inferred from a button state. | |
| * Step-up authentication must occur outside the co-browsing context where possible. | |
| * Transaction signing must bind the exact beneficiary, amount, and account to the customer’s authenticated approval. | |
| --- | |
| ## Medium-risk findings | |
| ### Domain allowlisting is not a sufficient boundary | |
| The docs require domains to be added to a widget key domain list, with examples such as `*example.com` or `*.example.com`. ([docs.surfly.com][1]) | |
| That helps prevent casual misuse of a widget key, but it does not solve the core risks. It does not constrain what the loaded JavaScript can do once it is running on an approved banking origin. Wildcard subdomain patterns can also become dangerous if any subdomain is vulnerable to takeover, XSS, or weak hosting controls. | |
| --- | |
| ### The documented loader lacks visible SRI protection | |
| The snippet does not show an `integrity` attribute or a pinned versioned URL. SRI is specifically designed to let browsers verify that a fetched script matches an expected cryptographic hash and detect unexpected tampering. ([OWASP Foundation][8]) | |
| For a banking platform, a mutable third-party script URL without SRI or version pinning is a major supply-chain concern. Dynamic SaaS JavaScript may be operationally convenient, but it is hard to reconcile with banking change-control requirements unless the bank has compensating controls such as self-hosting, a private build, strict CSP, hash monitoring, and contractual change notification. | |
| --- | |
| ### User metadata can become PII leakage | |
| Surfly allows user data such as name and email to be passed into sessions and displayed in queues/events. ([docs.surfly.com][6]) | |
| That means integration teams must treat Surfly session metadata as regulated customer data. Do not pass full customer identifiers, account numbers, phone numbers, national IDs, or unnecessary email addresses unless explicitly approved by privacy and legal teams. Use opaque internal support identifiers instead. | |
| --- | |
| ## Banking approval recommendation | |
| ### Do not approve this configuration | |
| I would reject the following pattern for banking: | |
| ```html | |
| <script> | |
| // Standard Surfly loader from surfly.com | |
| // included on authenticated banking pages | |
| </script> | |
| ``` | |
| especially if combined with: | |
| ```js | |
| cookie_transfer_enabled: true, | |
| cookie_transfer_proxying: true, | |
| host_switching_allowed: true, | |
| participants_can_request_to_interact: true, | |
| new_urls_allowed: true, | |
| non_hosts_can_open_tabs: true, | |
| allow_opening_urls_from_query_parameter: true, | |
| filesharing_enabled: true, | |
| screensharing_enabled: true, | |
| audit_logs_enabled: true, | |
| automatic_screenshots_enabled: true, | |
| private_session: false, | |
| password_required: false, | |
| admission_enabled: false | |
| ``` | |
| That configuration is too permissive for banking. | |
| --- | |
| ## Minimum hardening profile if the bank still wants Surfly | |
| The safer design is to **not load Surfly at all on the real online-banking app**. Instead, create a separate support surface that contains only the minimum information required for troubleshooting. | |
| A hardened Surfly-style configuration should look conceptually like this, with exact option names validated against your Surfly tenant/version: | |
| ```js | |
| Surfly.init({ | |
| widget_key: "REDACTED", | |
| // Keep Surfly out of ordinary browsing where possible | |
| embedded_sessions_only: true, | |
| session_start_confirmation: true, | |
| // Join/session protection | |
| private_session: true, | |
| password_required: true, | |
| admission_enabled: true, | |
| default_invitations_enabled: false, | |
| invitations_allowed: false, | |
| // No agent/customer role switching unless formally approved | |
| host_switching_allowed: false, | |
| participants_can_request_to_interact: false, | |
| // Prevent arbitrary navigation | |
| new_urls_allowed: false, | |
| non_hosts_can_open_tabs: false, | |
| allow_opening_urls_from_query_parameter: false, | |
| // Strict URL allowlisting | |
| allowlist: JSON.stringify([ | |
| { | |
| pattern: "^https://bank\\.example/(support-safe|help)(/|$)", | |
| type: "all" | |
| } | |
| ]), | |
| // Avoid transferring banking auth/session state | |
| cookie_transfer_enabled: false, | |
| cookie_transfer_proxying: false, | |
| enable_cookie_backtransfer: false, | |
| // Disable high-risk data movement | |
| filesharing_enabled: false, | |
| download_trigger_enabled: false, | |
| videochat: false, | |
| screensharing_enabled: false, | |
| automatic_screenshots_enabled: false, | |
| automated_session_recording_enabled: false, | |
| // Logs should be off unless privacy/legal approve exact fields and retention | |
| audit_logs_enabled: false, | |
| chat_logs_enabled: false, | |
| // Mask aggressively, but do not rely on masking as the main boundary | |
| hide_element_by_selector: | |
| "[data-sensitive], .balance, .account-number, .iban, .card-number, .pan, .otp, .pin, .statement, .transaction-history, form[action*='payment'] *" | |
| }); | |
| ``` | |
| That is still not enough by itself. The bank also needs server-side controls. | |
| --- | |
| ## Required server-side controls | |
| For a banking platform, the server must enforce the real security boundary: | |
| 1. **Block transactional endpoints during co-browsing.** Payments, transfers, beneficiary changes, password resets, OTP/PIN changes, card controls, personal-data updates, and document downloads should be blocked or forced into customer-only flows. | |
| 2. **Do not trust client-side Surfly state.** Headers such as Surfly detection headers should be treated carefully. Strip spoofable headers at the edge and only trust signals inserted by a controlled reverse proxy or trusted infrastructure. | |
| 3. **Use a separate support origin or path.** Example: `support.bank.example`, not the main authenticated banking app. | |
| 4. **No sensitive values in URLs.** Surfly’s own docs warn that masked values used in GET forms or URLs can still be exposed through logs/network activity. ([docs.surfly.com][5]) | |
| 5. **Require exact origin validation for messaging.** No wildcard `targetOrigin: "*"` for sensitive messages. | |
| 6. **Use least-privilege agent access.** Agents should authenticate with SSO/MFA, have scoped permissions, and be unable to join sessions without customer approval. | |
| 7. **Keep customer secrets out of the session.** OTPs, PINs, passwords, card CVVs, recovery codes, and transaction-signing prompts should never be visible or controllable in a co-browsing session. | |
| 8. **Use data-loss prevention on logs and recordings.** Prefer no screenshots, no recording, no chat logs, and no audit payloads containing form values unless explicitly required and redacted. | |
| --- | |
| ## Evidence I would demand from Surfly before approval | |
| Before using this in a bank, I would require: | |
| * Exact source code for the deployed `surfly.js`, including minified and unminified versions. | |
| * Version-pinned JavaScript artifact, immutable URL, and SRI-compatible hash. | |
| * SBOM for JavaScript, proxy, backend, browser-extension/session components, and dependencies. | |
| * Independent penetration test covering the Surfly proxy, session join links, PINs, WebSocket/session channels, masking, logging, screenshots, file transfer, and `postMessage` bridge. | |
| * Documentation of exactly what data the Surfly proxy can see in authenticated sessions. | |
| * Proof of how masking works: before data leaves the customer browser, inside the proxy, or only in the follower rendering. | |
| * Token/link entropy, TTLs, revocation behavior, brute-force protection, and leak handling. | |
| * Controls for follower-link sharing, admission, identity verification, and agent impersonation. | |
| * CSP guidance for banking pages. | |
| * Confirmation that Surfly can run with no cookie/localStorage/sessionStorage transfer. | |
| * Logs/recordings retention policy, encryption, deletion SLAs, and data residency guarantees. | |
| * A private/on-prem deployment option evaluation, since Surfly’s own docs mention on-prem installation as an option. ([docs.surfly.com][2]) | |
| --- | |
| ## Final risk rating | |
| **Default/public Surfly integration on authenticated banking pages: Critical risk — not approved.** | |
| **Hardened Surfly on a segregated, non-transactional support surface: Potentially approvable after source review, proxy review, strict configuration, independent pen test, legal/privacy review, and server-side transaction blocking.** | |
| **Use inside real online banking flows involving balances, payments, statements, KYC, credentials, OTPs, PINs, or card data: Not recommended.** | |
| The core reason is simple: Surfly’s value proposition is the same thing that makes it risky for banking. It gives a third-party script and proxy enough visibility and control to make co-browsing work. In a low-risk website, that may be acceptable. In a banking platform, that is a privileged session-interposition mechanism and must be treated like a high-risk extension of the bank’s trusted computing base. | |
| [1]: https://docs.surfly.com/surfly/javascript-api/quickstart "Quickstart | Documentation Portal" | |
| [2]: https://docs.surfly.com/surfly/technology/ "Surfly Technology | Documentation Portal" | |
| [3]: https://cheatsheetseries.owasp.org/cheatsheets/Third_Party_Javascript_Management_Cheat_Sheet.html?utm_source=chatgpt.com "Third Party JavaScript Management Cheat Sheet - OWASP" | |
| [4]: https://docs.surfly.com/surfly/session-options "Co-Browsing session options | Documentation Portal" | |
| [5]: https://docs.surfly.com/surfly/tutorials/security-functionalities "Security functionalities | Documentation Portal" | |
| [6]: https://docs.surfly.com/surfly/javascript-api/surfly-session "Sessions | Documentation Portal" | |
| [7]: https://docs.surfly.com/surfly/javascript-api/surfly-events "Events | Documentation Portal" | |
| [8]: https://owasp.org/www-community/controls/SubresourceIntegrity?utm_source=chatgpt.com "Subresource Integrity (SRI) - OWASP Foundation" |
Third report: Surfly apiframe.js telemetry and postMessage risk
Executive summary
The uploaded apiframe.js confirms two security-relevant behaviours that are not appropriate for core banking pages:
-
Surfly embeds Sentry telemetry and sends error/session/client-report data to a Sentry project at
sentry.io, using a visible DSN and release value. The code also attaches Surfly configuration, widget key, session ID, settings, anduser_datato the Sentry scope. -
The
postMessagetransport is mostly sent toSURFLY_COBRO_ORIGIN, but the Surfly protocol layer explicitly acceptsparams.targetOrigin === "*". That wildcard is used in the same internal messaging system that handles widget messages, cookie/storage restoration, session ending, and user messages.
The key concern is not just “there is Sentry” or “there is postMessage.” The concern is that both exist inside a co-browsing runtime that can interact with a banking session, browser storage, cookies, forms, navigation, tabs, and session control.
1. Sentry telemetry: where it goes
The uploaded apiframe.js contains a hardcoded Sentry DSN:
https://510f80906f4d429ba882cf65fdfbbca6@sentry.io/1545691
It also contains the release value:
4ed92455697daa4e4bf0aed94cffe820676bf8f7
The Sentry initialization is guarded by a replacement check, and if the DSN is present it initializes Sentry with that DSN and release. The code also installs GlobalHandlers, disables unhandled promise rejection capture in that specific global handler configuration, installs a frame-rewrite integration, and sets a Sentry tag of logger = jsapi.
Practically, events are sent to Sentry’s envelope endpoint for project 1545691, derived from the DSN. The effective destination is:
https://sentry.io/api/1545691/envelope/?sentry_key=510f80906f4d429ba882cf65fdfbbca6
The code includes Sentry transport logic that sends envelopes over fetch or XHR as POST requests, and builds envelope URLs from the DSN.
Banking meaning
This is a third-party telemetry egress path. Even if it is “only error reporting,” it can receive operational metadata from a customer’s banking session whenever Sentry captures an error, message, session update, or client report.
For banking, that means Sentry must be treated as a downstream processor/subprocessor and a potential leakage channel.
2. What Surfly attaches to Sentry
2.1 Surfly init settings
During Surfly.init(...), the code stores the user-provided settings and immediately calls Sentry setTags with:
user_settings: JSON.stringify(this._userSettings)
inside_session: this.isInsideSessionIt also sets:
widget_key: n.widget_keywhen a widget key is provided.
This is highly relevant. Anything passed into Surfly.init(...) can become Sentry metadata. If a bank’s integration team passes customer identifiers, CRM IDs, branch IDs, account context, risk segment, customer tier, support case details, or sensitive configuration into the Surfly settings object, that data can be attached to Sentry events.
2.2 Session ID and effective settings
When a session is created or queued, the code sets:
sessionId
settingson the Sentry scope:
setTag("sessionId", this._sessionId)
setExtra("settings", this.settings)This means later Sentry events can include the Surfly session ID and effective session configuration. In a banking platform, support-session IDs and session configuration are security-relevant metadata, especially if they can be correlated with customer journeys, agents, timestamps, or support cases.
2.3 User data passed to session start
Before starting a session, the code calls:
setExtra("user_data", n)This is one of the most important findings.
If the bank passes user data such as:
customer name
email
phone number
customer ID
account number
CRM ID
case number
risk score
VIP/private-banking flag
KYC status
product holdings
branch relationship manager
then that data can be attached as Sentry extra data on subsequent events.
For a banking deployment, this should be treated as not acceptable unless Surfly can prove Sentry is disabled or the data is scrubbed before transmission.
3. What Sentry may capture automatically
The bundle includes Sentry integrations for browser breadcrumbs and HTTP context. The Sentry code instruments console, DOM, fetch, history, and XHR, and creates breadcrumbs that are attached to later Sentry events.
3.1 Error events
The global error handler builds Sentry events from browser errors, including:
message
URL / filename
line
column
stack trace
error object
mechanism: onerror
handled: false
The uploaded code shows onerror capture is active, while onunhandledrejection is disabled in Surfly’s Sentry init configuration.
3.2 URL, referrer, and user-agent
The Sentry HttpContext integration adds request context using browser location, document referrer, and navigator user-agent.
In a banking SPA, this is risky because URLs can reveal:
customer IDs
account IDs
application IDs
statement IDs
transaction references
payment-flow state
KYC workflow state
internal route names
Even relative paths can be sensitive if they include identifiers or query strings.
3.3 Console breadcrumbs
The Sentry breadcrumb integration captures console calls. The breadcrumb contains the console level, joined message, and original arguments.
If the bank’s app or the Surfly integration logs API responses, customer objects, tokens, form values, error payloads, or debug objects, those can become Sentry breadcrumbs.
3.4 DOM breadcrumbs
The DOM breadcrumb logic builds element selectors using tag name, IDs, classes, and attributes such as:
aria-label
type
name
title
alt
The code avoids keypress breadcrumbs for INPUT, TEXTAREA, and contentEditable, so it does not appear to directly capture typed characters as DOM breadcrumbs. But it can still capture sensitive element names, labels, IDs, and class names.
For banking, selectors such as these can reveal business logic:
#beneficiary-account-number
input[name="otp"]
button[title="Approve transfer"]
.card-limit-form
.kyc-document-upload
3.5 XHR and fetch breadcrumbs
The bundle instruments XHR and fetch. Breadcrumbs include:
method
URL
status code
The instrumentation also sees request bodies internally as hints, even if the breadcrumb data object itself is primarily method, URL, and status.
This matters because banking APIs often contain sensitive identifiers in paths or query parameters, even when request bodies are not included.
4. Sentry risk rating for banking
| Sentry data path | Risk | Why |
|---|---|---|
Hardcoded Sentry DSN to sentry.io |
High | Third-party telemetry egress from banking pages |
user_settings: JSON.stringify(...) |
Critical | Can expose integration config and accidental PII |
widget_key tag |
High | Tenant/widget identifier leaves page |
sessionId tag |
High | Correlates errors to Surfly sessions |
settings extra |
High | Exposes effective Surfly configuration |
user_data extra |
Critical | Can expose customer PII if integrator passes it |
| URL/referrer/user-agent | High | Banking URLs often contain sensitive route context |
| Console breadcrumbs | High | Debug logs often leak sensitive objects |
| DOM breadcrumbs | Medium/high | Can reveal form/page semantics |
| XHR/fetch breadcrumbs | Medium/high | API URLs and status codes expose workflow details |
Sentry conclusion
For banking, Surfly’s Sentry must be one of:
disabled entirely,
routed to a bank-controlled Sentry project,
subject to strict beforeSend and beforeBreadcrumb scrubbing,
or removed in a private/on-prem build.
As uploaded, the code sends telemetry to Surfly/Sentry infrastructure and attaches too much integration/session metadata to be acceptable on sensitive banking pages.
5. postMessage: restricted or wildcarded?
The answer is nuanced but important:
Browser transport layer: mostly restricted to SURFLY_COBRO_ORIGIN.
Surfly protocol layer: explicitly wildcard-tolerant through params.targetOrigin === "*".
That distinction is the core issue.
6. Browser-level postMessage target
The central message sender builds a message:
{
sessionId: this._sessionId,
type: e,
params: n
}and sends it with:
postMessage(r, u.mainWindow.SURFLY_COBRO_ORIGIN)That means the actual browser postMessage target is not generally "*". It is sent to the Surfly co-browsing origin.
That is better than a raw:
postMessage(data, "*")However, this is not enough for a banking-grade design because the Surfly message object contains its own routing field called params.targetOrigin.
7. The critical issue: params.targetOrigin === "*"
The internal widget-message handler accepts a message if:
t.params.targetOrigin === "*"
||
t.params.targetOrigin === u.mainWindow.location.originThen it dispatches the message either as a user message or as an internal widget command:
t.params.target === "user"
? this._emit(h.MESSAGE, t.params)
: t.params.target === "widget" &&
(
od.hasOwnProperty(t.params.msg)
? od[t.params.msg].call(this, t)
: console.log("Got unknown widget message", t)
)This is the line that should be stressed:
t.params.targetOrigin === "*"In a banking-grade message bridge, that should not be acceptable for sensitive commands. A hardened implementation should require the exact expected origin, plus exact source-window validation, schema validation, command allowlisting, session nonce/correlation, and rejection of wildcard routing.
8. Wildcard used for cookie/storage restoration
The wildcard is not theoretical. The uploaded code uses it for internal widget messages.
The collect_web_storage handler checks cookie-transfer settings, collects localStorage and sessionStorage, optionally collects cookies, and then sends:
this._sendApiMessage("widget_message", {
srcOrigin: u.mainWindow.location.origin,
targetOrigin: "*",
target: "widget",
msg: "restore_cookies",
cookies: o,
storage: i
})That is a major banking concern because the same message contains:
srcOrigin
targetOrigin: "*"
target: "widget"
msg: "restore_cookies"
cookies
storage
Even though the outer browser postMessage is sent to SURFLY_COBRO_ORIGIN, the Surfly protocol-level message says: “this widget message is valid for any target origin.”
For banking, cookie and storage transfer should not exist on sensitive pages, and wildcard protocol routing should not be accepted for commands that can move browser state.
9. Wildcard used for session ending
The session end flow also uses:
targetOrigin: "*",
target: "widget",
msg: "end_session"when inside a Surfly session.
This is lower risk than cookie/storage restoration, but it confirms the wildcard is part of the design pattern, not a one-off.
10. Public API can also send wildcard payloads
The public sendMessage API defaults to the main window origin:
targetOrigin: n || u.mainWindow.location.originBut because n is caller-supplied, an integrator could pass "*":
session.sendMessage(message, "*")The code shown does not prevent that.
In a banking integration, the API should reject "*" outright for any message that crosses between bank page, Surfly iframe, proxied page, agent/follower context, or session widget.
11. Inbound origin and source checks
The code does include an inbound browser-message check. It listens for message events and first checks:
i.origin === s.SURFLY_COBRO_ORIGINThen it checks that the source is either the session window or the expected top-frame parent reference, depending on whether it is inside a Surfly session. Only then does it dispatch to Mi[...].
That is a positive control. It means the system is not simply accepting browser message events from arbitrary origins.
But it does not remove the concern about:
t.params.targetOrigin === "*"because the wildcard is inside the trusted Surfly message protocol after the browser-origin check has passed.
For banking, the rule should be stricter:
A message from Surfly’s origin is not automatically safe.
The message payload also needs strict validation.
Wildcard targetOrigin should never authorize sensitive routing.
12. Why params.targetOrigin === "*" is dangerous in banking
The danger is not that every website on the internet can directly post messages into the session. The danger is that Surfly’s own privileged message bridge accepts wildcard-targeted protocol messages for actions that can involve browser/session state.
In banking, that creates several risks:
12.1 Confused-deputy risk
A trusted Surfly frame can become a deputy that accepts broadly targeted commands and routes them into the bank page/session context. If any upstream Surfly component, agent-side context, embedded widget, or proxied session frame is compromised or misbehaves, the wildcard check reduces the precision of routing controls.
12.2 State-transfer risk
The wildcard appears directly in the path that can send cookies and web storage for restoration.
For banking, browser storage and cookies are part of the security boundary. A wildcard protocol target should never be involved in moving them.
12.3 Integration misuse risk
Because sendMessage accepts a caller-provided target origin, bank developers could accidentally use "*" in custom integration code.
That mistake would be easy to miss in review because the outer postMessage still appears to be restricted to SURFLY_COBRO_ORIGIN.
12.4 Weak auditability
A banking auditor looking only for postMessage(..., "*") might conclude the code is mostly restricted. But the real issue is subtler:
postMessage(..., SURFLY_COBRO_ORIGIN)combined with:
params.targetOrigin === "*"That means the wildcard exists at the protocol layer, not necessarily the browser transport layer. This is exactly the kind of issue auditors can miss if they do not review the application-level message router.
13. Combined Sentry + postMessage banking risk
The Sentry and postMessage findings amplify each other.
Surfly’s runtime:
- attaches user settings, widget key, session ID, settings, and user data to Sentry;
- collects breadcrumbs from console, DOM, XHR, fetch, and history;
- uses a message bridge for session/widget communication;
- allows
params.targetOrigin === "*"; - has cookie/storage restoration paths using
targetOrigin: "*"; - sends browser-level messages to Surfly-controlled origins;
- creates and controls session iframes.
In a bank, that is too much privilege and too many data paths for a third-party script on authenticated pages.
14. Required remediation before any banking consideration
14.1 Sentry remediation
Surfly should provide a banking build or tenant configuration where:
Sentry is disabled entirely,
or Sentry is routed to a bank-owned project,
or all Sentry events are scrubbed before transmission.
Minimum scrub requirements:
remove user_settings
remove widget_key
remove sessionId
remove settings
remove user_data
strip query strings and fragments from URLs
remove referrer
remove console arguments
remove DOM selector attributes
remove XHR/fetch URLs or reduce them to route templates
disable client reports unless approved
disable session tracking unless approved
The current code shows all the relevant scope attachment points exist.
14.2 postMessage remediation
Surfly should provide a hardened build/configuration where:
params.targetOrigin === "*" is removed,
targetOrigin: "*" is never generated,
sendMessage("*") is rejected,
incoming event.origin must equal the exact expected origin,
incoming event.source must equal the exact expected window,
every message type is schema-validated,
every command is allowlisted,
session nonce/correlation is mandatory,
cookie/storage-transfer messages are disabled or removed,
restore_cookies cannot be called on banking pages,
end_session and other widget commands require exact target origin.
The most important single remediation is:
Remove support for params.targetOrigin === "*".
Not “avoid using it.” Remove it.
15. Final banking conclusion
The uploaded code confirms that Surfly’s runtime sends telemetry to Sentry and uses a wildcard-tolerant internal message router.
The browser-level postMessage target is mostly restricted to SURFLY_COBRO_ORIGIN, which is good, but the internal Surfly router explicitly accepts:
t.params.targetOrigin === "*"and the runtime itself sends sensitive widget messages with:
targetOrigin: "*"including the restore_cookies path that can include cookies and web storage.
For ordinary support websites, this may be an acceptable engineering tradeoff. For a banking platform, it is not.
Recommendation: do not deploy this Surfly runtime on authenticated banking pages or transactional flows unless Surfly provides a hardened build with Sentry disabled or bank-controlled, cookie/storage transfer removed, and all wildcard params.targetOrigin === "*" handling eliminated.
Second report: Surfly
surfly.js/apiframe.jssecurity assessment for banking useBased on the artifacts uploaded on 15 May 2026, the position has moved from “high-level architectural concern” to confirmed code-level concern.
Executive verdict
I would not approve this integration for authenticated banking pages, online-banking dashboards, payment flows, onboarding/KYC flows, statements, card management, password/PIN/OTP flows, or any page containing regulated financial data.
The uploaded code confirms that Surfly is not a passive support button. It is a remote JavaScript runtime that:
localStorageandsessionStorage;postMessagecommand channels;z-index;For a normal sales/support website this may be acceptable with configuration controls. For a bank, this is a privileged session-interposition component. It should be treated closer to a browser extension, remote-control tool, session proxy, and third-party analytics/runtime combined.
1. What we have now learned
1.1 The public
surfly.jsfile is only a loaderThe uploaded
surfly.jsis not the main application. It is a small bootstrap file. It injects CSS, creates an iframe calledsurfly-api-frame, writes a blank document into it, and then loads:from
window.SURFLY_COBRO_ORIGIN. The loader also exposes the iframe globally aswindow.surflyApiFrame.The headers show this loader is served as
application/javascript, withcache-control: max-age=86400, last modified on 11 May 2026, and served bysurfly.com. The hash you uploaded for the loader was:Security meaning: the bank would not just be including a static file. It would be including a loader that pulls the real runtime from Surfly. Without SRI/version pinning, this is a mutable third-party dependency chain.
1.2 The real runtime is
apiframe.jsThe uploaded
apiframe.jsis the meaningful file. It is minified and has no source map available, so this is not a perfect source-level audit. But the code is clear enough to identify several major behaviours. The file includes bundled runtime code, Sentry, translation/polyfill/event-emitter code, session-control logic, state-transfer logic, cookie/storage-transfer logic, popup/overlay code, and Surfly/Webfuse API setup.The missing source map is important. It means we still cannot produce a complete SBOM or exact source-to-runtime trace. For banking approval, Surfly should provide source maps or unminified source under NDA.
2. Confirmed libraries and embedded components
The uploaded
apiframe.jsdoes not expose a cleanpackage.json, but fingerprinting shows the following components with reasonable confidence.@sentry/browser/ Sentry JS SDK7.120.4GlobalHandlersRewriteFrames~/static/bundles/widget/apiframe.jsBreadcrumbsfbemitter-styleEventEmitternode-polyglot-style translation systempunycodeobject.entries/object-keys/ intrinsic/polyfill helpersThe most security-relevant finding is Sentry. The bundle initializes Sentry with a real DSN and release hash:
and sets a
logger=jsapitag.The bundle also shows Sentry SDK metadata generation using
sentry.javascript.browserand package metadata of the formnpm:@sentry/browser, with version7.120.4.3. Highest-risk code behaviours
3.1 Same-page iframe injection without sandboxing
The loader creates an iframe with:
It then appends the iframe, writes an HTML document into it, and loads
apiframe.jsinto that document.This matters because the iframe is not created with a restrictive
sandboxattribute. It is effectively a privileged helper frame injected into the banking page. The script source is Surfly, but the script executes inside a document created by the bank page.Banking danger: this gives Surfly’s runtime a deep foothold inside the authenticated page environment. It is not equivalent to embedding a simple isolated SaaS iframe from another origin.
3.2 Form-state collection and restoration
The
apiframe.jscode walks the page and collects state from:It serializes values, checked states, disabled states, selected states, and
contentEditableinner HTML. The only explicit form-field exclusion visible in the snippet is_csrf.It also contains restore logic that writes those values back into the page:
and restores input values, checked states, disabled states, selected states, and
innerHTML.Banking danger: this is a direct risk to account numbers, balances, payment forms, beneficiary forms, card forms, loan forms, KYC fields, OTP-like fields if not properly isolated, and any sensitive data rendered into editable areas. Masking is not enough if the runtime can collect raw DOM/form state before or outside the masking boundary.
3.3 Web storage collection:
localStorageandsessionStorageThe bundle explicitly collects browser storage when cookie transfer is enabled and cookie proxying is not active:
It filters out keys beginning with
surfly_, then sends storage as part of arestore_cookieswidget message withtargetOrigin: "*".A separate helper shows the filter and output shape:
and again only excludes keys starting with
surfly_.Banking danger: many banking SPAs store sensitive or security-relevant state in web storage: feature flags, anti-fraud IDs, device binding hints, customer context, OAuth/OIDC transient state, API cache data, user profile data, UI state, or sometimes even tokens in weaker implementations. Surfly’s code path can collect and replay that state.
This is a major no-go unless storage transfer is disabled and independently verified.
3.4 Cookie-transfer and session-continuation logic
The runtime has explicit
restore_cookieshandling. It checks the source origin, referencescookie_transfer_enabledandcookie_transfer_proxying, and sends acookie_transfer_doneevent on failure or completion.When opening a session link, the code also checks:
and, if enabled, rewrites the session continuation path to:
otherwise it logs a cookie-transfer failure.
Banking danger: session cookies and continuation are core banking security assets. Even if Surfly’s intention is legitimate, any mechanism that transfers, proxies, restores, or continues authenticated session state creates a serious threat to session binding, fraud detection, device trust, CSRF assumptions, SameSite design, and step-up authentication.
For banking, cookie transfer/proxying should be treated as prohibited by default.
3.5
postMessagechannel with wildcard target origin supportThe runtime processes widget messages if:
or if the target origin equals the main window origin. It then dispatches messages either to the user event emitter or to internal widget handlers such as
restore_cookies.The storage-collection path sends a message with:
Banking danger: wildcard message handling is a design smell in high-assurance contexts. Even if other checks exist, the bank should not accept a sensitive integration where important control or state-transfer messages can be routed through wildcard-origin semantics. For banking, every message must have strict origin, schema, session, nonce, and command validation.
3.6 Session-control, tab-control, navigation, upload, chat, and video commands
The bundle defines a large command/event surface including:
and many tab/session events such as:
The runtime also exposes methods that post control messages to the Surfly iframe for tab control, relocation, pause/resume, and videochat mode/fullscreen changes.
Banking danger: these are exactly the capabilities that must be blocked around payments, beneficiaries, statements, profile changes, card controls, login recovery, and KYC. A co-browsing agent must not be able to trigger or influence transaction flow. Relying on front-end discipline is not enough.
3.7 Powerful iframe permissions
When creating the main session iframe, the code sets:
allowed for the Surfly co-browsing origin.
Banking danger: display capture and media permissions are not inherently malicious, but they are high-risk in regulated banking. They create additional channels for data exposure and social-engineering abuse. Even if user-consented, this needs strict business justification and technical restriction.
3.8 Full-page overlays and high z-index UI
The loader CSS gives
#surfly-api-framea very high z-index:and supports
surfly-blockerandsurfly-popupstates that can cover the full viewport.The runtime also includes blocker and popup CSS with fixed full-screen overlays and popup iframes.
Banking danger: full-page overlays are expected for support UX, but in banking they overlap with clickjacking, phishing, transaction-confusion, and agent-assisted fraud risks. A malicious, compromised, or incorrectly configured runtime could obscure or imitate banking UI.
3.9 Third-party telemetry via Sentry
The runtime initializes Sentry and includes integrations that capture browser context. The visible code includes Sentry
Breadcrumbs, which instruments console, DOM, XHR, fetch, history, and Sentry events.The Sentry global handlers capture
onerrorand error context; the HTTP context integration can add request URL, referrer, and user-agent data to events.Banking danger: telemetry can leak URLs, element selectors, error messages, stack traces, request metadata, console arguments, and user interaction breadcrumbs. This may include account identifiers, customer IDs, internal route names, transaction references, or sensitive page structure.
Even if Surfly/Sentry configure filters, this is another third-party data path that the bank must inventory, contractually control, redact, and test.
4. Threat scenarios specific to banking
Scenario A: Remote script compromise
If Surfly’s hosted script or build pipeline is compromised, the attacker does not need to exploit the bank directly. The bank has already invited Surfly’s JavaScript into the page. A malicious update could:
Impact: catastrophic.
Likelihood: not knowable from public code, but the impact is high enough that banks normally require version pinning, SRI, self-hosting, or private deployment review.
Scenario B: Sensitive form leakage through state transfer
The runtime collects inputs, selects, textareas, contenteditable HTML, same-origin iframe state, and scroll state. Only
_csrfis visibly excluded in the searched code.A banking page might contain:
Impact: regulated-data exposure and potential fraud.
Conclusion: unacceptable unless Surfly is completely excluded from pages containing sensitive fields.
Scenario C: Storage/token leakage
The runtime can collect
localStorageandsessionStorage, excluding only keys prefixed withsurfly_.Even if the bank follows best practice and keeps session tokens in HttpOnly cookies, storage may still contain sensitive state or useful attack context. If the bank has any SPA tokens, device IDs, customer context, cached API data, feature flags, or fraud metadata in web storage, this becomes a leakage path.
Impact: account compromise risk, privacy breach, fraud-control degradation.
Conclusion: storage transfer must be disabled and verified.
Scenario D: Session boundary weakening through cookie transfer
The bundle contains cookie-transfer and proxying logic, including a
/surfly_cookie_transfer/start/<token>flow when cookie transfer and proxying are enabled.Banking session controls depend on strict assumptions about where cookies live, how they are bound, how SameSite works, and how fraud systems interpret device/browser continuity.
Impact: undermines session-management assumptions.
Conclusion: cookie transfer/proxying should not be permitted in authenticated banking.
Scenario E: Agent-assisted transaction risk
The runtime exposes control, relocation, tab-control, file, chat, video, popup, and broadcast commands.
Even if agents are trusted, banking fraud often involves social engineering, insider risk, or accidental misuse. A support agent must not be placed in a position where they can influence payments, beneficiaries, card changes, loan submissions, password resets, or OTP/PIN entry.
Impact: unauthorized or disputed transactions.
Conclusion: transactional endpoints must be server-side blocked during co-browsing.
Scenario F: Telemetry leakage to Sentry
The bundle initializes Sentry with a real DSN and release hash, and the Sentry code instruments browser activity.
If an exception occurs on a banking page, captured context could include route URLs, DOM breadcrumbs, console arguments, request URLs, and metadata.
Impact: leakage of customer identifiers, internal app routes, transaction references, or page structure.
Conclusion: the bank must require telemetry disablement, redaction, or a private Sentry/project boundary with contractual controls and test evidence.
5. Banking risk rating
localStorageandsessionStoragecollection path existsOverall: Critical risk for authenticated banking use.
6. Is it “secure enough” for a banking platform?
For public marketing pages
Possibly acceptable, if no customer data, no authenticated state, no sensitive forms, and strict CSP/vendor monitoring are used.
For authenticated online banking
No. Not acceptable as-is.
The code has too much access to page state, storage, session mechanics, UI, navigation, and telemetry.
For support pages inside banking
Only possibly acceptable if isolated.
A safer pattern would be a separate support origin or support-only app that deliberately excludes balances, statements, transaction history, payments, beneficiaries, credentials, KYC documents, OTP/PIN fields, and card data.
For payments, transfers, beneficiary management, login recovery, OTP/PIN, card controls, or statements
Hard no.
Those flows should not include Surfly or any similar co-browsing runtime.
7. Minimum mandatory controls if the bank still wants to use Surfly
These are not “nice to have.” They are the minimum I would require before considering approval.
7.1 Do not load Surfly globally
Surfly must not be included in the base banking shell, global tag manager, common layout, or authenticated SPA root.
It should be loaded only on a dedicated, low-risk support surface.
7.2 Disable cookie and storage transfer
Required settings/concepts:
Then verify at runtime that:
collect_web_storagemessage is sent;restore_cookiesmessage is sent;/surfly_cookie_transfer/start/path is used;localStorage, orsessionStoragevalues leave the page.7.3 Block Surfly on sensitive routes
The bank should hard-block Surfly from:
This must be enforced server-side and in routing, not just by hiding the button.
7.4 Server-side transaction blocking during co-browse
When a co-browsing session is active or suspected:
Do not rely on client-side button disabling.
7.5 Disable or strictly constrain agent control
For banking, the default posture should be:
Exact option names should be verified against Surfly’s current config, but the policy is clear: view-only, no transaction control, no files, no screenshots, no arbitrary navigation.
7.6 Disable or isolate Sentry telemetry
The bank should require one of:
beforeSendfiltering and test evidence.The current bundle clearly initializes Sentry and browser telemetry code exists.
7.7 Require version pinning or self-hosting
The bank should not load:
as an unpinned mutable runtime in production banking.
Acceptable alternatives would be:
The current loader is served with a one-day cache and points to the next bundle dynamically.
8. What I would ask Surfly directly
Before any banking approval, ask Surfly for written answers and evidence:
cookie_transfer_enabled,cookie_transfer_proxying, and all storage transfer be completely disabled for a tenant?apiframe.js, source maps, and SBOM?postMessage, cookie transfer, storage transfer, iframe isolation, and agent-control abuse?If they cannot answer these with evidence, the integration should not proceed.
9. Final recommendation
Do not use this Surfly integration inside the real authenticated banking application.
A restricted deployment could be considered only if it is:
The most important finding from the uploaded code is this:
Surfly has code paths to observe, serialize, transfer, and restore browser state inside the user’s session. In banking, browser session state is not just UI state. It is part of the security boundary.
That makes the standard integration unsuitable for core banking.