Is RoQoL - Improve your Roblox experience safe?

High risk

RoQoL is high risk. When RoQoL needs authentication, it asks roqol.io for a Roblox OAuth redirect, submits the parameters to Roblox, follows the callback, and stores the returned auth key in storage. Later requests reuse that key in an Authentication header.

RoQoL Softwarev2.70.5Chrome Web Store
72Risk
Who publishes it

RoQoL Software - no other listings under this identity, 4 shared hostnames

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

Publisher
RoQoL Software

Shared hosts - 4 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.

roqol.io
Also called by 2 other listings, including RoQoL - Improve your Roblox experience
t2.rbxcdn.com
Also called by 3 other listings, including RoQoL - Improve your Roblox experience
css.rbxcdn.com
Also called by 4 other listings, including RoQoL - Improve your Roblox experience, Efaz's Roblox Extension
static.rbxcdn.com
Also called by 4 other listings, including RoQoL - Improve your Roblox experience, Roblox Bookmarker, Roblox 2019

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

Publishers can request a review.

Findings

Roblox OAuth approval stores a RoQoL auth key

When RoQoL needs authentication, it asks roqol.io for a Roblox OAuth redirect, submits the parameters to Roblox, follows the callback, and stores the returned auth key in storage.

Later requests reuse that key in an Authentication header.

Severity
High unwanted
Type
Unexpected
CWE
CWE-522
Source
Dynamic sandbox
What actually happens
You did this

You browse a Roblox page where RoQoL's content script is active.

The script asks the extension service worker for an authentication key when RoQoL features need one.

The extension did this

The extension can complete a Roblox OAuth authorization flow and save the returned RoQoL key.

The saved key is later available to RoQoL requests from the extension.

Captured request
GEThttps://roqol.io/api/oauth/?userId=123456789&noredirect=true

The code expects a JSON response with a Location value containing OAuth query parameters.

Captured request
POSThttps://apis.roblox.com/oauth/v1/authorizations

The code expects Roblox to return a location that the extension follows as the authentication callback.

Headers
Content-Type
application/json
Fields the source code assembles or stores during authentication
  • Your Roblox user ID
    123456789 (illustrative)

    This ties the OAuth request and the saved RoQoL key to your Roblox account.

  • OAuth client and challenge
    clientId, codeChallenge, codeChallengeMethod, nonce, redirectUri, state

    These values define the Roblox authorization request that the extension submits.

  • OAuth scopes
    [{"scopeType":"openid","operations":["read"]}] (illustrative)

    These entries describe what access is being requested from Roblox for your account.

  • Saved RoQoL auth key
    AuthKey: {"UserId":123456789,"Key":"6f1d8c24-0b78-4d6b-9f4b-2c2c44b9a0a1"} (illustrative)

    This key can authenticate later requests from the extension to RoQoL services.

The code that does this

The content script request and service-worker OAuth path

Readable version

Content script request, expanded

js/authentication/main.js
async function GetAuthKey() {  while (FetchingAuthKey) await sleep(100);  if (CachedAuthKey === "") {    FetchingAuthKey = true;    CachedAuthKey = await chrome.runtime.sendMessage({ type: "authentication" });    FetchingAuthKey = false;  }  return CachedAuthKey;}

Service worker authentication message route, expanded

js/background.js
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {  if (sender.id !== chrome.runtime.id) {    if (message.type === "installed") sendResponse(true);    return;  }  if (message.type === "authentication") {    GetAuthKey().then(function(authKey) {      sendResponse(authKey);    });    return true;  }  if (message.type === "forcereauthenticate") {    return LocalStorage.remove("AuthKey").then(function() {      CachedAuthKey = "";      GetAuthKey().then(function(authKey) {        sendResponse(authKey);      });    }), true;  }  if (message.type === "reauthenticate") {    ReauthenticateV2().then(function(authKey) {      sendResponse(authKey);    });    return true;  }});

OAuth flow, expanded

js/backgroundscripts/authenticationv2.js
async function GetOAuthKey(userId) {  if (CachedAuthKey !== "" && userId == LastAuthenticatedUserId) return CachedAuthKey;  let storedKey = await LocalStorage.get("AuthKey");  if (storedKey) {    storedKey = JSON.parse(storedKey);    if (storedKey.UserId == userId) {      CachedAuthKey = storedKey.Key;      LastAuthenticatedUserId = userId;      return CachedAuthKey;    }  }  const [startOk, startResult] = await RequestFunc(`${WebServerEndpoints.OAuth}?userId=${userId}&noredirect=true`, "GET");  if (!startOk) return "";  const params = new URLSearchParams(startResult.Location.split("?")[1]);  const scopes = params.get("scope").split(" " ).map(function(scope) {    let scopeType = scope;    let operations = ["read"];    const parts = scope.split(":");    if (parts.length === 2) {      scopeType = parts[0];      operations = [parts[1]];    } else if (parts.length > 2) {      scopeType = parts[0];    }    return { scopeType, operations };  });  const authorization = {    clientId: params.get("client_id"),    codeChallenge: params.get("code_challenge"),    codeChallengeMethod: params.get("code_challenge_method"),    nonce: params.get("nonce"),    redirectUri: params.get("redirect_uri"),    resourceInfos: [{ owner: { id: userId.toString(), type: "User" }, resources: {} }],    responseTypes: [params.get("response_type").charAt(0).toUpperCase() + params.get("response_type").slice(1)],    scopes,    state: params.get("state")  };  const [authorizeOk, authorizeResult] = await RequestFunc(    "https://apis.roblox.com/oauth/v1/authorizations",    "POST",    { "Content-Type": "application/json" },    JSON.stringify(authorization),    true,    false  );  if (!authorizeOk || !authorizeResult.location) return "";  const [callbackOk, callbackResult, callbackResponse] = await RequestFunc(    authorizeResult.location,    "GET",    { type: "Authentication" },    undefined,    false,    true  );  if (!callbackOk) return "";  CachedAuthKey = (await callbackResponse.json()).Key;  LocalStorage.set("AuthKey", JSON.stringify({ UserId: userId, Key: CachedAuthKey }));  AlertTabsOfNewAuthKey(CachedAuthKey);  return CachedAuthKey;}
The code that does this

RoQoL requests automatically attach the saved key outside auth endpoints

Readable version

Authentication header behavior, expanded

js/background.js
async function RequestFunc(url, method, headers, body, includeCredentials, bypassJson, retryAuth) {  headers ||= {};  const isRoqolRequest = url.search(WebServerURL) > -1;  if (retryAuth === undefined) retryAuth = true;  if (url.search("roblox.com") > -1) {    const parsed = new URL(url);    if (!parsed.searchParams.get("_roqolPlatform")) {      parsed.searchParams.set("_roqolRequest", "");    }    url = parsed.toString();    headers["x-csrf-token"] = CSRFToken;  } else if (isRoqolRequest && !url.includes("disabled_features")) {    if (headers?.Authentication !== undefined) {      retryAuth = false;    } else if ((!url.includes("/auth") && !url.includes("/oauth")) || url.includes("/reverify")) {      headers.Authentication = await GetAuthKey();    }  }  return fetch(url, {    method,    headers,    body,    credentials: includeCredentials === undefined ? undefined : includeCredentials ? "include" : "omit"  });}
Captured request
POSThttps://roqol.io/api/auth/v2/fetch

The fallback authentication path expects RoQoL to return a Key and UniverseId for favourite-based verification.

Hosts involved in the authentication path
    • roqol.io

    Receives authentication bootstrap requests, callback traffic, and later RoQoL API requests with the saved Authentication header.

    • apis.roblox.com

    Receives the OAuth authorization POST and other Roblox API requests made with browser credentials.

Updated 30 September 2026ohffojhdniagefegnmjfpfidnjofekpj