Is RoValra - Roblox Improved safe?
RoValra is high risk. RoValra can request a Roblox OAuth code at page init or when an API token is needed, fetch the callback URL, and store the token with your user ID and username. Testing saw no live request, needing a login; the shipped script has the flow.…
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Roblox OAuth token sent to RoValra during browsing
RoValra can request a Roblox OAuth code at page init or when an API token is needed, fetch the callback URL, and store the token with your user ID and username.
Testing saw no live request, needing a login; the shipped script has the flow.
- Severity
- High unwanted
- Type
- Unexpected
- CWE
- CWE-940
- Source
- Dynamic sandbox
You open Roblox while signed in and the RoValra content script initializes.
The same token helper also runs when a RoValra API request needs authentication.
The extension can request a Roblox OAuth code, fetch RoValra's callback URL, and save the returned token.
The source uses RoValra client ID 5835339573709822795 and the callback URL https://apis.rovalra.com/v1/auth/callback.
The source expects a JSON location value and passes that returned URL to the RoValra callback fetch.
The callback response is accepted only when it contains status success, access_token, user_id, and username.
- OAuth client ID5835339573709822795
Ties the Roblox authorization request to RoValra's OAuth application.
- Callback destinationhttps://apis.rovalra.com/v1/auth/callback
Sends the authorization result to RoValra's API host.
- Access tokenBearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkiLCJhdWQiOiI1ODM1MzM5NTczNzA5ODIyNzk1In0.X9f9mK6f5JpC5m2aQ8Yb4w (illustrative)
Can authenticate later RoValra API requests linked to your Roblox account.
- Roblox account identityuser_id=123456789, username=BuilderRbx (illustrative)
Links the stored token record to a Roblox user ID and username.
- Requested scopesopenid:read, profile:read
Allows RoValra to read basic OpenID and profile information through the OAuth grant.
The storage shape written after a successful callback; values are illustrative since the available run recorded no live token.
- Location
- chrome.storage.local key 'rovalra_oauth_verification'
{ "123456789": { "robloxId": "123456789", "username": "BuilderRbx", "verified": true, "timestamp": 1752360000000, "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkiLCJhdWQiOiI1ODM1MzM5NTczNzA5ODIyNzk1In0.X9f9mK6f5JpC5m2aQ8Yb4w" }}Shipped OAuth and bearer-token code paths
RoValra API wrapper attaches the bearer token
content.js async function callRobloxApi(options) { if (options.subdomain === "gamejoin") { let useGameJoinV2 = await refreshGameJoinVersionPreference(); options = { ...options, endpoint: normalizeGameJoinEndpoint( options.endpoint, useGameJoinV2 ) }; } if (options.subdomain === "gamejoin" && (options.method || "GET").toUpperCase() === "POST" && options.body && typeof options.body == "object" && !(options.body instanceof FormData)) { let bodyUpdate = {}; options.endpoint?.split("?")[0].startsWith("/v2/") && (bodyUpdate.joinOrigin = "RoValraFetchInfo"), options.body.gameJoinAttemptId || (bodyUpdate.gameJoinAttemptId = self.crypto.randomUUID()), options = { ...options, body: { ...options.body, ...bodyUpdate } }; } if (options.subdomain === "gamejoin") { let now = Date.now(), nextAllowedTime = lastGameJoinRequestTime + 100; now < nextAllowedTime ? (lastGameJoinRequestTime = nextAllowedTime, await new Promise( (resolve) => setTimeout(resolve, nextAllowedTime - now) )) : lastGameJoinRequestTime = now; } let requestKey = getRequestKey(options), shouldCache = !options.noCache && options.subdomain !== "gamejoin"; if (shouldCache && activeRequests.has(requestKey)) return (await activeRequests.get(requestKey)).clone(); let requestPromise = (async () => { let { endpoint, subdomain = "apis", method = "GET", isRovalraApi = !1, headers = {}, body = null, fullUrl: customFullUrl, skipAutoAuth = !1, signal, useBackground = !1, useApiKey = !1, noCache = !1, responseType = "text" } = options, normalizedHeaders = new Headers(headers); if (isRovalraApi && subdomain === "apis" && normalizedHeaders.set( "x-rovalra-user-agent", getRovalraUserAgent() ), useApiKey) { let apiKey = await getValidApiKey(); apiKey && normalizedHeaders.set("x-api-key", apiKey); } if (useBackground) return new Promise((resolve) => { chrome.runtime.sendMessage( { action: "fetchRobloxApi", options: { endpoint, subdomain, fullUrl: customFullUrl, method, body, headers: Object.fromEntries( normalizedHeaders.entries() ), noCache, responseType } }, (response2) => { if (chrome.runtime.lastError || !response2) { resolve(Response.error()); return; } useApiKey && response2.status === 401 && invalidateApiKey(); let { body: body2, ...init126 } = response2; resolve(new Response(body2, init126)); } ); }); if (isRovalraApi && subdomain === "apis") { if (!skipAutoAuth) { let token = await getValidAccessToken(); token && normalizedHeaders.set("Authorization", `Bearer ${token}`); } if (await checkSimulatedDowntime()) return console.warn( `RoValra API: [SIMULATION] 500 Error for ${endpoint}` ), new Response( JSON.stringify({ errors: [ { code: 500, message: "Simulated Internal Server Error" } ] }), { status: 500, statusText: "Internal Server Error", headers: { "Content-Type": "application/json" } } ); await checkSimulatedLatency() && (console.warn( `RoValra API: [SIMULATION] Adding 5s latency for ${endpoint}` ), await new Promise((resolve) => setTimeout(resolve, 5e3))); } if (subdomain === "gamejoin") { if (await checkSimulatedJoinHttpError()) return console.warn( `RoValra API: [SIMULATION] Returning 500 error for ${endpoint}` ), new Response( JSON.stringify({ errors: [ { code: 500, message: "Simulated Internal Server Error" } ] }), { status: 500, statusText: "Internal Server Error", headers: { "Content-Type": "application/json" } } ); if (await checkSimulatedJoinError()) throw console.warn( `RoValra API: [SIMULATION] Throwing network error for ${endpoint}` ), new Error("ERR_SOCKS_CONNECTION_FAILED"); } let baseUrl = isRovalraApi ? subdomain === "www" ? "https://www.rovalra.com" : `https://${subdomain}.rovalra.com` : `https://${subdomain}.roblox.com`, fullUrl = customFullUrl || `${baseUrl}${endpoint}`; fullUrl.includes("?") ? fullUrl += `&_RoValraRequest=${noCache ? Date.now() : ""}` : fullUrl += `?_RoValraRequest=${noCache ? Date.now() : ""}`; let isMutatingMethod = ["POST", "PATCH", "DELETE"].includes( method.toUpperCase() ), credentials = options.credentials ?? (isRovalraApi ? "omit" : "include"); normalizedHeaders.has("Accept") || normalizedHeaders.set("Accept", "application/json"); let fetchOptions = { method, headers: normalizedHeaders, credentials, signal, cache: noCache ? "no-store" : "default" }; if (body && (body instanceof FormData ? (fetchOptions.body = body, normalizedHeaders.has("Content-Type") && normalizedHeaders.delete("Content-Type")) : (normalizedHeaders.has("Content-Type") || normalizedHeaders.set("Content-Type", "application/json"), fetchOptions.body = typeof body == "string" ? body : JSON.stringify(body))), !isRovalraApi && !useBackground) try { let authenticatedUserId = await getAuthenticatedUserId(), batHeaders = await hbaClient.generateBaseHeaders( fullUrl, method, !!authenticatedUserId, fetchOptions.body ); batHeaders["x-bound-auth-token"] && normalizedHeaders.set( "x-bound-auth-token", batHeaders["x-bound-auth-token"] ); } catch (err4) { console.warn("RoValra API: Failed to generate BAT token", err4); } if (isRovalraApi) { let lastResponse, authRetried = !1; for (let attempt = 0; attempt < 4; attempt++) { try { lastResponse = await fetch(fullUrl, fetchOptions); let newAccessToken = null; try { let bodyClone = await lastResponse.clone().json(); bodyClone && bodyClone.accessToken && (newAccessToken = bodyClone.accessToken); } catch { } if (newAccessToken) try { let authedUserId = await getAuthenticatedUserId(); if (authedUserId) { let allVerifications = (await chrome.storage.local.get( OAUTH_STORAGE_KEY ))[OAUTH_STORAGE_KEY] || {}, storedVerification = allVerifications[authedUserId]; if (storedVerification) { console.log( "RoValra API: New token detected in body. Updating storage." ), storedVerification.accessToken = newAccessToken, storedVerification.timestamp = Date.now(); try { let data = await lastResponse.clone().json(); } catch { } allVerifications[authedUserId] = storedVerification, await chrome.storage.local.set({ [OAUTH_STORAGE_KEY]: allVerifications }); } } } catch (e) { console.error( "RoValra API: Failed to update new access token.", e ); } let isTokenInvalid = lastResponse.status === 401, bodyIsInvalid = !1; if (lastResponse.ok && endpoint && endpoint.includes("/v1/auth") && !skipAutoAuth) { let clonedForBodyCheck = lastResponse.clone(); try { let bodyJson = await clonedForBodyCheck.json(); bodyJson.status === "error" && (bodyJson.message === "Invalid or obsolete token." || bodyJson.message === "Invalid or obsolete session.") && (isTokenInvalid = !0, bodyIsInvalid = !0, console.log( "RoValra API: Invalid token/session from response body detected." )); } catch { } } if (isTokenInvalid && endpoint && endpoint.includes("/v1/auth") && !skipAutoAuth) { if (!authRetried) { console.log( "RoValra API: Invalid token/session, attempting token refresh..." ), authRetried = !0; let newToken = await getValidAccessToken( !0, !1 ); if (newToken) { fetchOptions.headers.set( "Authorization", `Bearer ${newToken}` ); continue; } } console.warn( "RoValra API: Authentication failed repeatedly. Clearing storage as last resort." ), await chrome.storage.local.remove(OAUTH_STORAGE_KEY); break; } if (lastResponse.ok && !bodyIsInvalid) return lastResponse; if (endpoint && endpoint.includes("/v1/auth")) break; } catch (error2) { if (attempt === 3 || endpoint && endpoint.includes("/v1/auth")) throw console.error( `RoValra API: Request to ${fullUrl} failed${attempt === 3 ? " after multiple retries" : ""}.`, error2 ), error2; } attempt < 3 && await new Promise((res) => setTimeout(res, 1e3)); } return lastResponse.ok || console.error( `RoValra API: Request to ${fullUrl} failed with status ${lastResponse.status} after multiple retries.` ), lastResponse; } if (isMutatingMethod) { let csrfToken = await getCsrfToken(); csrfToken && normalizedHeaders.set("X-CSRF-TOKEN", csrfToken); } let timeoutId = null, abortSignalCleanup = null, didGameJoinTimeout = !1; if (subdomain === "gamejoin" && isGameJoinTimeoutEnabled(endpoint)) { let controller = new AbortController(); timeoutId = setTimeout(() => { didGameJoinTimeout = !0, controller.abort(); }, GAMEJOIN_TIMEOUT_MS), signal && (signal.aborted ? controller.abort() : (abortSignalCleanup = /* @__PURE__ */ __name(() => controller.abort(), "abortSignalCleanup"), signal.addEventListener("abort", abortSignalCleanup, { once: !0 }))), fetchOptions.signal = controller.signal; } let cleanupGameJoinTimeout = /* @__PURE__ */ __name(() => { timeoutId && (clearTimeout(timeoutId), timeoutId = null), signal && abortSignalCleanup && (signal.removeEventListener("abort", abortSignalCleanup), abortSignalCleanup = null); }, "cleanupGameJoinTimeout"), response; try { response = await fetch(fullUrl, fetchOptions); } catch (error2) { if (cleanupGameJoinTimeout(), didGameJoinTimeout) return createGameJoinFullResponse(); if (error2.name === "AbortError" || signal && signal.aborted) return new Response(null, { status: 499, statusText: "Client Closed Request" }); throw error2; } if (response.status === 403 && isMutatingMethod) { let newCsrfToken = response.headers.get("x-csrf-token"); if (newCsrfToken) { typeof getCsrfToken.setToken == "function" && getCsrfToken.setToken(newCsrfToken), fetchOptions.headers.set("X-CSRF-TOKEN", newCsrfToken); try { response = await fetch(fullUrl, fetchOptions); } catch (error2) { if (cleanupGameJoinTimeout(), didGameJoinTimeout) return createGameJoinFullResponse(); if (error2.name === "AbortError" || signal && signal.aborted) return new Response(null, { status: 499, statusText: "Client Closed Request" }); throw error2; } } } try { subdomain === "gamejoin" && (response = await normalizeGameJoinResponse(response)); } catch (error2) { if (didGameJoinTimeout) return createGameJoinFullResponse(); throw error2; } finally { cleanupGameJoinTimeout(); } return response.ok || (console.error( `RoValra API: Request to ${fullUrl} failed with status ${response.status}.` ), useApiKey && response.status === 401 && await invalidateApiKey()), response; })(); shouldCache && (activeRequests.set(requestKey, requestPromise), requestPromise.finally(() => activeRequests.delete(requestKey))); let originalResponse; try { originalResponse = await requestPromise; } catch (err4) { let errorMessage2 = err4.message || "Unknown network error"; throw options.subdomain === "gamejoin" && (gameJoinErrorCount++, gameJoinErrorCount > 3 && document.dispatchEvent( new CustomEvent("rovalra-gamejoin-critical-error", { detail: { errorMessage: `Network error: ${errorMessage2}` } }) )), err4; } let clonedResponse = originalResponse.clone(), errorMessage = `HTTP error: ${originalResponse.status} ${originalResponse.statusText}`; if (options.subdomain === "gamejoin" && !originalResponse.ok && originalResponse.status !== 429 && (gameJoinErrorCount++, gameJoinErrorCount > 3 && originalResponse.clone().json().then((data) => { data?.errors?.[0]?.message && (errorMessage += ` - ${data.errors[0].message}`), document.dispatchEvent( new CustomEvent("rovalra-gamejoin-critical-error", { detail: { errorMessage } }) ); }).catch(() => { document.dispatchEvent( new CustomEvent("rovalra-gamejoin-critical-error", { detail: { errorMessage } }) ); })), options.subdomain === "gamejoin" && originalResponse.ok && originalResponse.clone().json().then((data) => { if (data?.joinScript?.SessionId) try { if (typeof data.joinScript.SessionId == "string" && data.joinScript.SessionId.startsWith("{")) { let sessionId = JSON.parse( data.joinScript.SessionId ); typeof sessionId.Latitude == "number" && typeof sessionId.Longitude == "number" && updateUserLocationIfChanged({ userLat: sessionId.Latitude, userLon: sessionId.Longitude }); } } catch { } if (data.status === 5) { let serverId = null; try { let bodyData = options.body && typeof options.body == "string" ? JSON.parse(options.body) : options.body; bodyData && bodyData.gameId && (serverId = bodyData.gameId); } catch { } serverId && document.dispatchEvent( new CustomEvent("rovalra-server-inactive", { detail: { serverId } }) ); } }).catch(() => { }), options.subdomain === "games" && options.endpoint.includes("/servers/") && !options.isRovalraApi) try { let monitorClone = clonedResponse.clone(), fullUrl = `https://${options.subdomain || "games"}.roblox.com${options.endpoint}`; monitorClone.json().then((data) => { document.dispatchEvent( new CustomEvent("rovalra-game-servers-response", { detail: { url: fullUrl, data } }) ); }).catch(() => { }); } catch (e) { console.warn("RoValra API: Monitor hook failed", e); } return clonedResponse; }OAuth token lookup starts the OAuth flow
content.js async function getValidAccessToken(forceRefresh = !1, lazyForNonDonators = !0) { let userId = await getAuthenticatedUserId(); if (!userId) return null; if (await shouldUseFallback()) return console.log("RoValra: Using fallback authentication (skipping OAuth)"), await clearOAuthProgress(), await getValidFallbackToken(forceRefresh); let isDonator = getCurrentUserTier2() >= 1, allVerifications = (await chrome.storage.local.get(STORAGE_KEY3))[STORAGE_KEY3] || {}, storedVerification = allVerifications[userId], isAccountSwitch = Object.keys(allVerifications).length > 0 && !storedVerification, existingProgress = await getOAuthProgress(); if (existingProgress?.data?.userId && String(existingProgress.data.userId) !== String(userId) && (console.log( "RoValra: OAuth progress belongs to another user. Clearing." ), await clearOAuthProgress()), !isDonator && lazyForNonDonators && !storedVerification && !isAccountSwitch) return console.log( "RoValra: Non-donator lazy mode - skipping OAuth generation until explicitly needed." ), null; if (!storedVerification || !storedVerification.accessToken) return await startOAuthFlow(!0) ? (await chrome.storage.local.get(STORAGE_KEY3))[STORAGE_KEY3]?.[userId]?.accessToken || null : (console.log("RoValra: OAuth failed, trying fallback..."), await clearOAuthProgress(), await getValidFallbackToken(forceRefresh)); if (storedVerification && String(storedVerification.robloxId) !== String(userId)) return console.warn( "RoValra: Stored OAuth ID mismatch. Proactively re-authenticating." ), await startOAuthFlow(!0) ? (await chrome.storage.local.get(STORAGE_KEY3))[STORAGE_KEY3]?.[userId]?.accessToken || null : (console.log("RoValra: OAuth failed (wrong user), trying fallback..."), await clearOAuthProgress(), await getValidFallbackToken(forceRefresh)); if (!forceRefresh) return storedVerification.accessToken; try { let response = await callRobloxApi({ isRovalraApi: !0, subdomain: "apis", endpoint: "/v1/auth/settings", headers: { Authorization: `Bearer ${storedVerification.accessToken}` }, skipAutoAuth: !0, noCache: !0 }); return response.status === 401 || response.status === 403 ? (console.warn( `RoValra: Session unauthorized (Status ${response.status}). Triggering re-auth...` ), !isDonator && lazyForNonDonators ? null : await startOAuthFlow(!0) ? (await chrome.storage.local.get(STORAGE_KEY3))[STORAGE_KEY3]?.[userId]?.accessToken || null : (console.log("RoValra: OAuth re-auth failed, trying fallback..."), await clearOAuthProgress(), await getValidFallbackToken(!0))) : (await chrome.storage.local.get(STORAGE_KEY3))[STORAGE_KEY3]?.[userId]?.accessToken || storedVerification.accessToken; } catch (error2) { return console.error("RoValra: Network error during token sync:", error2), storedVerification.accessToken; } }OAuth authorization and callback handling
content.js async function startOAuthFlow(silent = !1) { let userId = await getAuthenticatedUserId(); if (!userId) return !1; if (!silent) return console.warn( "RoValra: Non-silent OAuth flow is not implemented as per the background-only request." ), Promise.resolve(!1); if (activeOAuthPromise) return activeOAuthPromise; activeOAuthPromise = (async () => { try { let currentProgress = await getOAuthProgress(), now = Date.now(), elapsed = currentProgress ? now - (currentProgress.timestamp || 0) : 1 / 0; if (currentProgress && currentProgress.step && String(currentProgress.data?.userId) === String(userId)) { if (elapsed < 6e4) return console.log("RoValra: Resuming recent OAuth process..."), await resumeOAuthFlow( userId, currentProgress ) ? !0 : (console.log( "RoValra: Resumption attempt failed. Too recent to redo steps." ), !1); console.log( "RoValra: OAuth flow stale (> 1 min). Restarting." ), await clearOAuthProgress(); } console.log("RoValra: Starting new OAuth flow..."), console.log("RoValra: Checking birthdate..."); let birthResponse = await callRobloxApi({ subdomain: "users", endpoint: "/v1/birthdate", method: "GET" }); if (birthResponse.ok) { let data = await birthResponse.json(), { birthYear, birthMonth, birthDay } = data, today = /* @__PURE__ */ new Date(), age = today.getFullYear() - birthYear, m2 = today.getMonth() + 1 - birthMonth; if ((m2 < 0 || m2 === 0 && today.getDate() < birthDay) && age--, age < 13) return console.log( "RoValra: User is under 13. Will use fallback auth." ), await clearOAuthProgress(), !1; } await saveOAuthProgress("birthdate_checked", { userId }), await saveOAuthProgress("existence_verified", { userId }); try { console.log( "RoValra: Attempting direct OAuth authorization POST request..." ); let response = await callRobloxApi({ subdomain: "apis", endpoint: "/oauth/v1/authorizations", method: "POST", body: { clientId: "5835339573709822795", responseTypes: ["Code"], redirectUri: "https://apis.rovalra.com/v1/auth/callback", scopes: [ { scopeType: "openid", operations: ["read"] }, { scopeType: "profile", operations: ["read"] } ], resourceInfos: [ { owner: { id: userId.toString(), type: "User" }, resources: {} } ] } }); if (response.ok) { let locationUrl = (await response.json()).location; return locationUrl ? (await saveOAuthProgress("got_auth_code", { userId, locationUrl }), await resumeOAuthFlow(userId, { step: "got_auth_code", data: { userId, locationUrl } })) : !1; } } catch (error2) { return console.error("RoValra: OAuth authorization error", error2), !1; } } catch (error2) { return console.error("RoValra: OAuth flow failure", error2), !1; } })(); try { return await activeOAuthPromise; } finally { activeOAuthPromise = null; } } async function resumeOAuthFlow(userId, progress) { let { step, data } = progress; if (data?.userId && String(data.userId) !== String(userId)) return await clearOAuthProgress(), !1; try { if (step === "birthdate_checked") return await saveOAuthProgress("existence_verified", { userId }), await resumeOAuthFlow(userId, { step: "existence_verified", data: { userId } }); if (step === "got_auth_code") { if ((await chrome.storage.local.get(STORAGE_KEY3))[STORAGE_KEY3]?.[userId]?.accessToken) return console.log( "RoValra: Token already present, clearing progress." ), await clearOAuthProgress(), !0; let { locationUrl } = data; console.log("RoValra: Resuming token fetch from callback..."); let tokenResponse = await callRobloxApi({ fullUrl: locationUrl, method: "GET", isRovalraApi: !0, skipAutoAuth: !0, noCache: !0 }); if (!tokenResponse.ok) return !1; let tokenData = await tokenResponse.json(); if (tokenData.status === "success" && tokenData.access_token && tokenData.user_id && tokenData.username) { let allVerifications = (await chrome.storage.local.get(STORAGE_KEY3))[STORAGE_KEY3] || {}; return allVerifications[userId] = { verified: !0, robloxId: tokenData.user_id, username: tokenData.username, accessToken: tokenData.access_token, timestamp: Date.now() }, await chrome.storage.local.set({ [STORAGE_KEY3]: allVerifications }), await clearOAuthProgress(), !0; } return !1; } if (step === "existence_verified") { let response = await callRobloxApi({ subdomain: "apis", endpoint: "/oauth/v1/authorizations", method: "POST", body: { clientId: "5835339573709822795", responseTypes: ["Code"], redirectUri: "https://apis.rovalra.com/v1/auth/callback", scopes: [ { scopeType: "openid", operations: ["read"] }, { scopeType: "profile", operations: ["read"] } ], resourceInfos: [ { owner: { id: userId.toString(), type: "User" }, resources: {} } ] } }); if (response.ok) { let locationUrl = (await response.json()).location; return locationUrl ? (console.log( "RoValra: Got authorization code. Fetching token from callback URL..." ), await saveOAuthProgress("got_auth_code", { userId, locationUrl }), await resumeOAuthFlow(userId, { step: "got_auth_code", data: { userId, locationUrl } })) : (console.error( "RoValra: OAuth authorization response did not contain a location URL." ), await clearOAuthProgress(), !1); } else return console.error( "RoValra: OAuth authorization POST request failed with status " + response.status ), !1; } return console.warn("RoValra: Unknown OAuth progress step:", step), !1; } catch (error2) { return console.error("RoValra: Error resuming OAuth flow:", error2), await clearOAuthProgress(), !1; } }- apis.roblox.com
Receives the OAuth authorization POST for RoValra client ID 5835339573709822795.
- apis.rovalra.com
Receives the OAuth callback request and later RoValra API requests with bearer-token authentication.
- users.roblox.com
Receives the birthdate check that the code runs before proceeding with OAuth for signed-in users.
MAIN World Injection Intercepts All Roblox Network Traffic
On any roblox.com page, fetch and XHR are replaced before load, so every request's URL, method, and body pass through the extension's code. 25 hook events were seen.
With 'Roblox API Docs' on, it logs every unique endpoint locally.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-940
- Source
- Dynamic sandbox
You open any page on roblox.com.
The extension replaces window.fetch and XMLHttpRequest with its own versions before the page starts executing.
Because the script runs in the MAIN world at document_start, the page's own JavaScript sees the patched functions and has no way to detect or bypass the interception.
The interception setup in intercept.js (lines 30-75)
fetch replacement — readable
// Save the real fetch, then replace it.const originalFetch = window.fetch;window.fetch = async function(...args) { const [url, config] = args; const method = config?.method ?? 'GET'; // Broadcast the request URL, method, and body as a DOM event // so the content script (isolated world) can read it. dispatchCaptureEvent(url, method, config?.body); // Proceed with the real request. let response = await originalFetch(...args); // Optionally rewrite response body for streamer-mode privacy masking. // ... return response;};XHR replacement — readable
// Save the real open/send, then replace both.const originalOpen = XMLHttpRequest.prototype.open;const originalSend = XMLHttpRequest.prototype.send;XMLHttpRequest.prototype.open = function(method, url, ...rest) { // Stash the URL and method on the XHR instance for use in send(). this._rovalra_url = url; this._rovalra_method = method; return originalOpen.apply(this, [method, url, ...rest]);};XMLHttpRequest.prototype.send = function(...args) { // Broadcast request details as a DOM event before sending. dispatchCaptureEvent(this._rovalra_url, this._rovalra_method, args[0]); return originalSend.apply(this, args);};- Request URLhttps://games.roblox.com/v1/games/5030456/servers/Public?sortOrder=Asc&limit=25
The full URL of every network request the page makes, including any query parameters.
- HTTP methodPOST
Whether the request is a GET, POST, or other type, indicates reads vs writes.
- Request body{"userAssetIds":[12345678,23456789]}
The data sent with POST or PUT requests, may include form inputs, JSON payloads, or Roblox game actions.
With 'Roblox API Docs' on, the extension catalogs every unique endpoint contacted on roblox.com, with example bodies, stored locally only.
- Location
- chrome.storage.local key 'rovalra_captured_apis'
{ "apis": { "/matchmaking-api/v1/client-status": { "GET": { "exampleBody": null, "exampleEndpoint": "/matchmaking-api/v1/client-status?placeId=5030456&gameId=abc-123" } } }, "games": { "/v1/games/5030456/servers/Public": { "GET": { "exampleBody": null, "exampleEndpoint": "/v1/games/5030456/servers/Public?sortOrder=Asc&limit=25" } } }, "catalog": { "/v1/catalog/items/details": { "POST": { "exampleBody": "{\"items\":[{\"itemType\":\"Asset\",\"id\":13512411}]}", "exampleEndpoint": "/v1/catalog/items/details" } } }}The Chrome Web Store listing describes RoValra as an extension that provides features other Roblox tools paywall. The manifest's content_scripts entry specifying `world: "MAIN"` and `run_at: "document_start"` means the network interception is active on every roblox.com page load regardless of which extension features you use or whether you have any features enabled. The extension does not present a consent prompt or notification about the interception at startup.
During dynamic analysis, we confirmed the interception is operational: 25 XHR send hook events and 148 cookie.get calls were recorded in 120 seconds on roblox.com pages.
Run this in the Chrome DevTools console on any roblox.com page to check whether window.fetch and XMLHttpRequest have been replaced by RoValra's intercept.js.
- Chrome with DevTools access
- RoValra extension installed
// rovalra-intercept-check.js// Paste into Chrome DevTools console on any roblox.com page.// Reports whether window.fetch and XMLHttpRequest.prototype.send// have been replaced by the RoValra extension.(function() { const results = {}; // Check if window.fetch is native const fetchSrc = window.fetch.toString(); results.fetchPatched = !fetchSrc.includes('[native code]'); results.fetchSource = fetchSrc.slice(0, 200); // Check if XMLHttpRequest.prototype.send is native const xhrSendSrc = XMLHttpRequest.prototype.send.toString(); results.xhrSendPatched = !xhrSendSrc.includes('[native code]'); results.xhrSendSource = xhrSendSrc.slice(0, 200); // Check for the sentinel flag set by the interceptor results.rovalraInterceptorFlag = !!window.__ROVALRA_INTERCEPTOR_SETUP__; console.table(results); if (results.fetchPatched || results.xhrSendPatched) { console.warn('[ROVALRA-CHECK] Network APIs are patched. RoValra intercept.js is active.'); } else { console.log('[ROVALRA-CHECK] Network APIs appear native. Interceptor not detected.'); }})();- 1Install RoValra.
- 2Navigate to https://www.roblox.com/.
- 3Open DevTools (F12), Console.
- 4Paste and run this script.
- 5A table shows whether fetch/XHR are patched, plus the first 200 characters of their source.