Is RoQoL - Improve your Roblox experience safe?
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.
Who publishes itRoQoL Software - no other listings under this identity, 4 shared hostnames
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.
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.
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
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 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.
The code expects a JSON response with a Location value containing OAuth query parameters.
The code expects Roblox to return a location that the extension follows as the authentication callback.
- Content-Type
- application/json
- Your Roblox user ID123456789 (illustrative)
This ties the OAuth request and the saved RoQoL key to your Roblox account.
- OAuth client and challengeclientId, 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 keyAuthKey: {"UserId":123456789,"Key":"6f1d8c24-0b78-4d6b-9f4b-2c2c44b9a0a1"} (illustrative)
This key can authenticate later requests from the extension to RoQoL services.
The content script request and service-worker OAuth path
Content script request, expanded
js/authentication/main.jsasync 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.jschrome.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.jsasync 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;}RoQoL requests automatically attach the saved key outside auth endpoints
Authentication header behavior, expanded
js/background.jsasync 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" });}The fallback authentication path expects RoQoL to return a Key and UniverseId for favourite-based verification.
- 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.