Is Webcam + Screen Video Recorder Video Editor safe?
StoryXpress Screen Recorder captures your screen, active tab, and webcam and uploads the video in chunks to its servers.
When you record, the extension uses chrome.tabCapture and getUserMedia in an offscreen document to capture the chosen tab or screen plus webcam audio/video, then POSTs the recording in chunks to wisp.storyxpress.co. Throughout each recording it sends operational telemetry (userId, workspaceId, videoId, OS and device info) to api.storyxpress.co, reports upload failures to a hardcoded Slack webhook, and reads storyxpress.co cookies that it forwards to content scripts injected on LinkedIn, Gmail, HubSpot, and other workplace sites. The bundle ships several hardcoded API credentials in cleartext, including a static client secret refetched every hour and an expired JWT whose embedded secret remains exposed.
Who publishes itStoryXpress, Inc. - no other listings under this identity, 2 shared hostnames
StoryXpress, Inc. - no other listings under this identity, 2 shared hostnames
What this publisher told the store about itself, and the other listings that told it the same thing.
Shared hosts - 2 hostnames
Hostnames hardcoded in this extension that few other listings call. That can mean one operator behind both, and it can equally mean a small shared vendor, so it is context rather than a conclusion. Hosts that many listings call are left out: they are services, not connections.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Hardcoded JWT Exposes Client Secret in All API Calls
Every API call, analytics, uploads, logging, carries a hardcoded JWT.
It expired April 2022, but its credential (788bd71fd1e5d86dd2e1184e1649d6499446ae2c) stays compiled in, sent to api.storyxpress.co per call.
Installers can extract it.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-522
- Source
- Dynamic sandbox
You install the extension and it begins any background API activity.
The extension attaches a hardcoded JWT to every outbound API request as the Authorization and CAT headers.
The same token appears 19+ times across background.bundle.js and every content script. It is sent to /logging/loki, /analytics/by-email/basic-metrics, /cdn/file/, and /workspace/quick-record-companion.
The JWT looks like a random string on the wire, but it is a standard base64-encoded token. Pasting it into jwt.io or running the decoder below reveals the embedded client secret immediately.
{ "alg": "HS256", "typ": "JWT"}.{ "clientId": "clapboard", "secret": "788bd71fd1e5d86dd2e1184e1649d6499446ae2c", "iat": 1562585487, "exp": 1648985487}The hardcoded token as it appears in the shipping source:
Context: the header is not generated at runtime — it is a string literal
// slackNotification() — video upload failure reporter// background.bundle.js ~line 12131const headers = { Authorization: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRJZCI6ImNsYXBib2FyZCIsInNlY3JldCI6Ijc4OGJkNzFmZDFlNWQ4NmRkMmUxMTg0ZTE2NDlkNjQ5OTQ0NmFlMmMiLCJpYXQiOjE1NjI1ODU0ODcsImV4cCI6MTY0ODk4NTQ4N30.XY2W-TkXNPt9e3BhZkTEBUISN2qxxfoqRFooe2W3gmY", // Decoded: { clientId: 'clapboard', secret: '788bd71fd1e5d86dd2e1184e1649d6499446ae2c', exp: 1648985487 }};// The same literal appears as both Authorization and CAT headers// across /logging/loki, /analytics/by-email/basic-metrics,// /cdn/file/, and /workspace/quick-record-companion endpoints.Decodes the hardcoded JWT from the extension source and prints the embedded client secret. Run with Node.js, no dependencies required.
- Node.js 14+
// storyxpress-jwt-decode.js// Decodes the hardcoded JWT from StoryXpress Screen Recorder v5.0.0// and prints the embedded client secret.// Run: node storyxpress-jwt-decode.jsconst token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRJZCI6ImNsYXBib2FyZCIsInNlY3JldCI6Ijc4OGJkNzFmZDFlNWQ4NmRkMmUxMTg0ZTE2NDlkNjQ5OTQ0NmFlMmMiLCJpYXQiOjE1NjI1ODU0ODcsImV4cCI6MTY0ODk4NTQ4N30.XY2W-TkXNPt9e3BhZkTEBUISN2qxxfoqRFooe2W3gmY';const parts = token.split('.');const header = JSON.parse(Buffer.from(parts[0], 'base64').toString());const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());console.log('Header:', JSON.stringify(header, null, 2));console.log('Payload:', JSON.stringify(payload, null, 2));console.log();console.log('clientId:', payload.clientId);console.log('secret: ', payload.secret);console.log('issued: ', new Date(payload.iat * 1000).toISOString());console.log('expired: ', new Date(payload.exp * 1000).toISOString());- 1Save this file.
- 2Run: node storyxpress-jwt-decode.js.
- 3The output shows the clientId and embedded credential from the hardcoded token.
- api.storyxpress.co
StoryXpress backend API. Gets the hardcoded JWT as Authorization/CAT headers on /logging/loki, /analytics/by-email/basic-metrics, /cdn/file/, /workspace/quick-record-companion.
Slack Webhook URL Exposes Internal Channel to Anyone with the CRX
When a video upload fails, the extension posts the video ID, status, and error code to a StoryXpress Slack channel via a hardcoded webhook URL.
The URL grants write access; anyone extracting it can post arbitrary Slack, in several files.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-319
- Source
- Dynamic sandbox
A video upload to StoryXpress CDN fails for any reason.
The extension POSTs a failure notification containing the video ID, HTTP status, and error code to a hardcoded Slack webhook URL.
The webhook URL is embedded in plain text in background.bundle.js, offscreen.bundle.js, videoPreview.bundle.js, and gmailScript.bundle.js. Anyone who downloads the CRX can extract it and post arbitrary messages to the same channel.
Slack webhook accepts any POST with a valid JSON body, no additional authentication required beyond the URL itself
- Content-Type
- application/json
{ "text": "CB -[PROD]: Video upload failure | upload_failed | Status: 503 | videoID: sx_a3f9b21c4d7e"}The slackNotification function and hardcoded URL:
Deobfuscated slackNotification function — webhook URL is a plain string literal
// slackNotification() — background.bundle.js:12131const slackNotification = async (status, statusCode, videoId, mentionTeam = false) => { const message = `CB -[PROD]: Video upload failure | ${status} | Status: ${statusCode} | videoID: ${videoId}`; await postToSlack({ url: 'https://hooks.slack.com/services/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', message: { text: message + (mentionTeam ? ' - <@U03PMBKSFS7> <@U03RXGNBPTQ>' : '') }, callbackFunction: () => console.info('Notification sent to Slack!') });};- hooks.slack.com
Slack incoming webhook endpoint for workspace T02NTPHMG. Receives video ID and upload error details. The hardcoded URL also grants write access to this channel from any origin.
Hardcoded API Credentials Authenticate Hourly Token Rotation
On install, the extension GETs tsapi.storyxpress.co/internal/client/code with a hardcoded client ID and credential; the server accepted these (HTTP 200) in tests.
It repeats hourly, storing the code in IndexedDB.
Credentials aren't revoked.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-522
- Source
- Dynamic sandbox
You install the extension, no further interaction needed.
The extension immediately calls the StoryXpress token endpoint with hardcoded credentials and stores the returned auth code.
The same call repeats every hour. The credentials are compiled into the source and cannot be changed without publishing a new extension version.
HTTP 200, server accepted the hardcoded credentials and returned an auth code (ts_code) stored in extension IndexedDB
- sxorigin
- chrome-extension://jfpjkgdpgdbddknpgplfkjjfncenlmkf
- Content-Type
- application/json
The credential call and its hourly schedule in background.bundle.js:
Deobfuscated credential rotation function
// fu — called on startup and every 3600 seconds// background.bundle.js:19398async function fetchAuthToken { const creds = { clientId: 'storyxpress_recorder', // hardcoded secret: 'btNR973AvSkm', // hardcoded config: 'recorder' }; const response = await callInternalClientCode(creds); if (response?.success && response?.data) { const code = response.data[0].code; await indexedDB.set({ ts_code: code }); // stored for subsequent API auth storage.setValue('ts_code', code); }}// Scheduled at startup:setInterval(fetchAuthToken, 3_600_000); // repeats every hourThe request builder (sa/ca functions)
// sa / ca — the underlying fetch call// background.bundle.js:14035function callInternalClientCode(creds) { const path = `internal/client/code?clientId=${creds.clientId}&secret=${creds.secret}&config=${creds.config || 'recorder'}`; const headers = { 'Content-Type': 'application/json', sxorigin: `chrome-extension://${chrome.runtime.id}` }; return fetchFromBase(path, headers, 'https://tsapi.storyxpress.co');}On every browser startup
Credentials sent once on extension startup, then every 3600 seconds while the browser is running.
- tsapi.storyxpress.co
StoryXpress recording API. Gets the hardcoded clientId and credential at /internal/client/code, returns a session auth code used for later calls.
What it can do
Permissions this extension asks for, as declared in version 5.0.0. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Read and change your data on every site you visit
*://*/*
See the address and title of every tab you have open
tabs
Show you desktop notifications
notifications
Read and change cookies, including the ones that keep you signed in
cookies
Start, monitor and manage your downloads
downloads
Capture the video and audio of a tab
tabCapture
Act on the current tab, but only after you click the extension
activeTab
Store data in your browser
storage
Write to your clipboard
clipboardWrite
Run its own code inside the pages you visit
scripting
Run hidden pages in the background
offscreen
Where it sends data
Destinations our analysis observed StoryXpress contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- hooks.slack.com
StoryXpress sends data to hooks.slack.com. 5 other extensions we have analysed send data here.
- api.storyxpress.co
StoryXpress sends data to api.storyxpress.co. No other extension we have analysed sends data here.
- tsapi.storyxpress.co
StoryXpress sends data to tsapi.storyxpress.co. No other extension we have analysed sends data here.
- wisp.storyxpress.co
StoryXpress sends data to wisp.storyxpress.co. No other extension we have analysed sends data here.