Is Webcam + Screen Video Recorder Video Editor safe?

Medium risk

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.

clapboard-acessv5.0.0Chrome Web Store
55Risk
Who publishes it

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.

Publisher
clapboard-acess
Declared legal entity
StoryXpress, Inc.
Registered address
44331 Apache Circle, Ashburn, VA 20147, US
Registered contact
Chinmay Jindal

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.

app.klenty.com
Also called by 1 other listing: Klenty: Email Outreach & Tracking from Gmail
storyxpress.co
Also called by 2 other listings, including Schema Builder for Structured Data

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
What actually happens
You did this

You install the extension and it begins any background API activity.

The extension did this

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.

Why you can't catch this in DevTools

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.

Decoded value
{  "alg": "HS256",  "typ": "JWT"}.{  "clientId": "clapboard",  "secret": "788bd71fd1e5d86dd2e1184e1649d6499446ae2c",  "iat": 1562585487,  "exp": 1648985487}
The code that does this

The hardcoded token as it appears in the shipping source:

Readable version

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.
Reproduce it yourself

Decodes the hardcoded JWT from the extension source and prints the embedded client secret. Run with Node.js, no dependencies required.

Requires
  • Node.js 14+
storyxpress-jwt-decode.js · js
// 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());
How to run it
  1. 1Save this file.
  2. 2Run: node storyxpress-jwt-decode.js.
  3. 3The output shows the clientId and embedded credential from the hardcoded token.
Endpoints that receive the hardcoded JWT on every call
    • 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
What actually happens
You did this

A video upload to StoryXpress CDN fails for any reason.

The extension did this

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.

Captured request
POSThttps://hooks.slack.com/services/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

Slack webhook accepts any POST with a valid JSON body, no additional authentication required beyond the URL itself

Headers
Content-Type
application/json
Body
{  "text": "CB -[PROD]: Video upload failure | upload_failed | Status: 503 | videoID: sx_a3f9b21c4d7e"}
The code that does this

The slackNotification function and hardcoded URL:

Readable version

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!')  });};
Where the upload failure data is sent
    • 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
What actually happens
You did this

You install the extension, no further interaction needed.

The extension did this

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.

Captured request
GEThttps://tsapi.storyxpress.co/internal/client/code?clientId=storyxpress_recorder&secret=btNR973AvSkm&config=recorder

HTTP 200, server accepted the hardcoded credentials and returned an auth code (ts_code) stored in extension IndexedDB

Headers
sxorigin
chrome-extension://jfpjkgdpgdbddknpgplfkjjfncenlmkf
Content-Type
application/json
The code that does this

The credential call and its hourly schedule in background.bundle.js:

Readable version

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 hour

The 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');}
When this fires

On every browser startup

Credentials sent once on extension startup, then every 3600 seconds while the browser is running.

Where the hardcoded credentials are sent
    • 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.

Updated 30 September 2026jfpjkgdpgdbddknpgplfkjjfncenlmkf