Is Secure Exam Proctor safe?

Medium risk

Secure Exam Proctor conceals all server domain names and API paths inside integer byte arrays throughout its code.

The extension stores more than 30 strings — including its server hostnames, API route segments, and a hardcoded AES-CBC decryption key — as Uint8Array integer sequences that must be decoded at runtime rather than appearing as readable text. This pattern covers every network-facing constant: the proctorauth.com API base, the proctor.io WebSocket host, and the telemetry.proctorcollect.com endpoint. Standard review tools that scan for domain names in source code will not surface these destinations without first running the decode step.

Proctoriov1.5.26135.119Chrome Web Store
45Risk
Who publishes it

Proctorio Inc. - no other listings under this identity, 1 shared hostname

What this publisher told the store about itself, and the other listings that told it the same thing.

Publisher
Proctorio
Declared legal entity
Proctorio Inc.
Registered address
7340 East Main Street, Scottsdale, AZ 85251, US
Registered contact
Mike Olsen

Shared hosts - 1 hostname

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.

purl.imsglobal.org
Also called by 3 other listings, including LTI Debugger

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-506
SourceAI SANDBOX

Server domains and API paths stored inside byte arrays

The extension builds server domains, API paths, and crypto IDs at runtime from byte arrays, not text.

A hardcoded AES-CBC fallback key decodes from a Uint8Array via TextDecoder.

A PoC decoded 108 such patterns, unsearchable as plaintext.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You install the extension and it begins a proctored session.

The extension did this

The extension assembles server addresses and API paths from integer byte arrays; they are not written anywhere as readable text in the source code.

A static analysis tool or curious developer searching the source for 'proctorauth.com/scm/session/create' will find nothing; the string is constructed at call time.

02EvidenceOPAQUE REVEAL
Why you can't catch this in DevTools

This is how the 'proctorauth.com/scm/' base path appears in the shipped source. Without running the code, the destination server is not visible to a human reader.

What's actually being sent
proctorauth.com/scm/
03EvidenceCODE COMPARE
The code that does this

How session/create and session/end endpoints are constructed at runtime

What it actually does
session/create — readable form
// Wo() returns 'proctorauth.com/scm/'
// 'session/create' is decoded inline from [115,101,115,115,105,111,110,47,99,114,101,97,116,101]
const endpoint = `https://${subdomain}${isStaging ? '-staging' : ''}.proctorauth.com/scm/session/create`;
const response = await fetch(endpoint, { method: 'POST', body: formData, headers: authHeaders }, 60000, 1);
session/end — readable form
// 'session/end' is decoded inline from [115,101,115,115,105,111,110,47,101,110,100]
const endpoint = `https://${subdomain}${isStaging ? '-staging' : ''}.proctorauth.com/scm/session/end`;
await fetch(endpoint, { method: 'POST', body: formData, headers: authHeaders }, 60000, 1);
AES-CBC fallback key — readable form
// Primary path: decrypt WebSocket payload with session key from server
try {
  return JSON.parse(atob(await decryptAES(parts[1], sessionKey, parts[0])));
} catch (e) {
  // Fallback: decrypt with hardcoded 32-byte key 'AF1sUGvsvUsbjx5nr04OTzZ9CFYULUXw'
  const fallbackKey = new Uint8Array([65,70,49,115,85,71,118,115,118,85,115,98,106,120,53,110,114,48,52,79,84,122,90,57,67,70,89,85,76,85,88,119]);
  return JSON.parse(atob(await decryptAES(parts[1], fallbackKey, parts[0])));
}
04EvidenceTHIRD PARTY LIST
Destinations constructed from encoded strings in the shipped source:
  • proctorauth.com

    Primary backend. Session lifecycle endpoints (session/create, session/end) POST to <subdomain>.proctorauth.com/scm/.

  • cdn.proctorauth.com

    Content delivery origin for the extension's proctor assets.

  • proctor.io

    Primary product domain. WebSocket endpoints (gbl4845ws.proctor.io, gbl0317ws.proctor.io) constructed from encoded strings for real-time proctoring traffic.

  • us1.gabbath.com

    Session URL encoded in Js2Q.js: 'https://us1.gabbath.com/session?deviceid=&userid='. Purpose unclear from static analysis alone.

  • telemetry.proctorcollect.com

    Telemetry endpoint. Domain appears both as plaintext and was identified in related static analysis.

05EvidenceARTIFACT
Reproduce it yourself

Scans the extension's main service worker (Js2Q.js) for all Uint8Array byte sequences and decodes them to plaintext, revealing domain names, API paths, cryptographic identifiers, and the hardcoded AES-CBC fallback key. Run it against a local copy of the extension.

RequiresNode.js 18+ (for TextDecoder built-in)The extracted extension directory containing assets/Js2Q.js
secure-exam-proctor-decode-strings.js · js
/**
 * Decode Uint8Array-encoded strings from Secure Exam Proctor (fpmapakogndmenjcfoajifaaonnkpkei)
 * Claim 6701 reproducer — CWE-506 (Embedded Malicious Code / string obfuscation)
 *
 * Usage:
 *   node secure-exam-proctor-decode-strings.js /path/to/extracted/assets/Js2Q.js
 */

const fs = require('fs');
const path = require('path');

const jsPath = process.argv[2] || path.join(__dirname, 'assets/Js2Q.js');
if (!fs.existsSync(jsPath)) {
  console.error('File not found:', jsPath);
  process.exit(1);
}

const js = fs.readFileSync(jsPath, 'utf8');
const decoder = new TextDecoder();
const regex = /new Uint8Array\(\[([^\]]+)\]\)/g;
let m;
const results = [];

while ((m = regex.exec(js)) !== null) {
  const bytes = m[1].split(',').map(Number);
  try {
    const decoded = decoder.decode(new Uint8Array(bytes));
    results.push({ pos: m.index, length: bytes.length, text: decoded });
  } catch (e) { /* ignore non-UTF-8 */ }
}

console.log(`Total new Uint8Array([...]) patterns in file: ${results.length}`);
console.log('\nAll decoded strings:\n');
results.forEach(({ pos, length, text }) => {
  console.log(`  offset ${pos.toString().padStart(7)} [${length} bytes]: ${JSON.stringify(text)}`);
});

// Highlight the hardcoded AES-CBC fallback key
const KEY_BYTES = [65,70,49,115,85,71,118,115,118,85,115,98,106,120,53,110,114,48,52,79,84,122,90,57,67,70,89,85,76,85,88,119];
const key = decoder.decode(new Uint8Array(KEY_BYTES));
console.log('\n=== Hardcoded AES-CBC fallback decryption key ===');
console.log(`  Value: ${JSON.stringify(key)}`);
console.log(`  Length: ${key.length} bytes`);
console.log('  Location: catch-block at ~offset 375114 in Js2Q.js');
console.log('  Used when: server-supplied session key fails to decrypt WebSocket payload');
How to run it
  1. 1
    Download the extension and unzip the .crx.
  2. 2
    Run: node secure-exam-proctor-decode-strings.js /path/to/extracted/assets/Js2Q.js.
  3. 3
    Review the output: Uint8Array patterns decode, including the hardcoded AES-CBC fallback key.
Updated 30 September 2026fpmapakogndmenjcfoajifaaonnkpkei