Is RoRegion – Reduce Lag, Ping & Boost FPS: Roblox Region Selector safe?

High risk

RoRegion is high risk. The Upgrade button starts a Roblox purchase from the background worker: it gets a CSRF token via your cookies, then POSTs purchase-with-robux with an EXP key and 75 or 500 price. DA saw the CSRF request and price lookups, not the purchase.…

kadenliarev13Chrome Web Store
75Risk

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

Publishers can request a review.

Findings

SeverityHIGH
ClassUNWANTED
TypeUnexpected
CWECWE-506
SourceAI SANDBOX

Roblox cookies can authorize a Robux subscription purchase

The Upgrade button starts a Roblox purchase from the background worker: it gets a CSRF token via your cookies, then POSTs purchase-with-robux with an EXP key and 75 or 500 price.

DA saw the CSRF request and price lookups, not the purchase.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You click the Upgrade button in the RoRegion membership panel.

The button is enabled when the page has a subscription target key for the selected tier.

The extension did this

The extension asks the background service worker to complete a Roblox subscription purchase.

The background flow uses your logged-in Roblox cookie context to request a CSRF token and prepare the purchase request.

02EvidenceFIELD TABLE
Fields that authorize or parameterize the purchase flow
FieldValueWhy it matters
Your Roblox session cookies
ROBLOSECURITY=<redacted> (illustrative)They let the request run as your logged-in Roblox account instead of as an anonymous visitor.
Roblox CSRF token
x-csrf-token response header from auth.roblox.com/v2/logoutThis token is required before Roblox accepts account-changing POST requests from the browser session.
Subscription target key
EXP-7702603079316800003This chooses the Roblox subscription product that the purchase request points to.
Robux price
75 or 500This is the amount of Robux the request says should be charged for the subscription tier.
Roblox user ID
5743677898After a successful purchase, the extension records which Roblox account should receive the subscription grant.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://auth.roblox.com/v2/logout
Observed during dynamic analysis as an empty authenticated POST used to obtain the x-csrf-token response header; no token was returned without a valid Roblox login.
04EvidenceNETWORK CAPTURE
Captured request
GEThttps://api.roregion.com/rr-sub-info/75
Observed during dynamic analysis as a live server-controlled subscription tier lookup that returns subscriptionTargetKey and tier data.
05EvidenceCODE COMPARE
The code that does this

The shipped code obtains a Roblox CSRF token and builds the subscription purchase POST

What it actually does
Readable CSRF token and purchase POST pathbackground.js
async function rrFetchCsrfToken() {
  const res = await fetch('https://auth.roblox.com/v2/logout', {
    method: 'POST',
    credentials: 'include'
  });
  const token = res.headers.get('x-csrf-token');
  if (!token) throw new Error('Could not fetch CSRF token');
  return token;
}

async function rrPurchaseSubscription(subscriptionTargetKey, priceInRobux) {
  const xsrf = await rrFetchCsrfToken();
  const res = await fetch(
    `https://apis.roblox.com/v1/subscriptions/purchase-with-robux/${encodeURIComponent(subscriptionTargetKey)}`,
    {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': xsrf
      },
      body: JSON.stringify({ priceInRobux })
    }
  );
  let data = null;
  try { data = await res.json(); } catch (e) { data = null; }
  return { httpStatus: res.status, data };
}
Readable Upgrade button purchase messageregionSelector.js
purchaseBtn.addEventListener('click', async () => {
  if (purchaseBtn.disabled) return;
  if (!cfg.subscriptionTargetKey) return;

  const chargeAmount = (typeof cfg.priceInRobux === 'number' && cfg.priceInRobux > 0)
    ? cfg.priceInRobux
    : cfg.price;

  let liveRobux = null;
  try {
    const balRes = await chrome.runtime.sendMessage({ action: 'fetchRobuxBalance' });
    if (balRes && balRes.success) liveRobux = balRes.robux;
  } catch (e) {}

  if (liveRobux !== null && liveRobux < chargeAmount) {
    errorDiv.textContent = notEnoughRobux_Translated;
    purchaseLabel.textContent = upgrade_Translated;
    purchaseBtn.disabled = false;
    return;
  }

  let purchaseResult = null;
  try {
    purchaseResult = await chrome.runtime.sendMessage({
      action: 'rrPurchaseSubscription',
      subscriptionTargetKey: cfg.subscriptionTargetKey,
      priceInRobux: chargeAmount
    });
  } catch (e) {
    purchaseResult = { success: false, error: e.message };
  }

  if (!purchaseResult || !purchaseResult.success || !purchaseResult.data || !purchaseResult.data.isSuccess) {
    let msg = purchaseFailed_Translated;
    if (purchaseResult && purchaseResult.error === 'insufficient_robux') {
      msg = notEnoughRobux_Translated;
    }
    errorDiv.textContent = msg;
    purchaseLabel.textContent = upgrade_Translated;
    purchaseBtn.disabled = false;
    return;
  }

  try {
    await chrome.runtime.sendMessage({
      action: 'rrRecordSubscription',
      subscriptionTargetKey: cfg.subscriptionTargetKey,
      priceInRobux: chargeAmount,
      tier: 'unlimited'
    });
  } catch (e) {}

  try {
    await chrome.storage.local.set({ rr_tool_tier: 'unlimited' });
  } catch (e) {}

  _rrMembershipCache = null;
  memberContent.innerHTML = '';
  const successCard = document.createElement('div');
  successCard.id = 'rr-purchase-success';
  successCard.style.background = isDarkMode ? 'rgba(45,138,78,0.10)' : 'rgba(45,138,78,0.07)';
  successCard.style.border = `1px solid ${isDarkMode ? 'rgba(45,138,78,0.30)' : 'rgba(45,138,78,0.25)'}`;

  const successEmoji = document.createElement('div');
  successEmoji.className = 'rr-ps-emoji';
  successEmoji.textContent = '🎉';

  const successTitle = document.createElement('div');
  successTitle.className = 'rr-ps-title';
  successTitle.style.color = isDarkMode ? '#7adb9a' : '#2d8a4e';
  successTitle.textContent = purchaseSuccess_Translated;

  const successSub = document.createElement('div');
  successSub.className = 'rr-ps-sub';
  successSub.style.color = isDarkMode ? '#aaa' : '#666';
  successSub.textContent = purchaseSuccessSub_Translated;

  successCard.append(successEmoji, successTitle, successSub);
  memberContent.appendChild(successCard);

  setTimeout(() => {
    renderMembershipUI(true);
  }, 2200);
});
06EvidenceCODE COMPARE
The code that does this

The subscription keys and tier selection are hardcoded in the page script

What it actually does
const RR_PROVIDER_ID = '5743677898';
const RR_SUB_KEYS = {
  75: 'EXP-7702603079316800003',
  500: 'EXP-5049868274708382266'
};

function rrStartPricingPrefetch() {
  if (_rrPricingPromise) return _rrPricingPromise;
  _rrPricingPromise = (async () => {
    const balancePromise = chrome.runtime.sendMessage({ action: 'fetchRobuxBalance' })
      .then(r => r && r.success && typeof r.robux === 'number' ? r.robux : null)
      .catch(() => null);

    const productsPromise = fetch(
      `https://apis.roblox.com/v1/subscriptions/active-subscription-products?subscriptionProductType=1&subscriptionProviderId=${RR_PROVIDER_ID}`,
      { credentials: 'include' }
    )
      .then(resp => resp.ok ? resp.json() : null)
      .then(json => (json && json.subscriptionProductsInfo) || [])
      .catch(() => null);

    const [balance, products] = await Promise.all([balancePromise, productsPromise]);
    const tier = balance !== null && balance >= 75 && balance < 500 ? 75 : 500;
    const subscriptionTargetKey = RR_SUB_KEYS[tier];
    const match = products && subscriptionTargetKey
      ? products.find(p => p.subscriptionTargetKey === subscriptionTargetKey)
      : null;
    const priceInRobux = match && typeof match.priceInRobux === 'number' ? match.priceInRobux : null;

    return {
      tier,
      displayPrice: priceInRobux !== null ? priceInRobux : tier,
      priceInRobux,
      subscriptionTargetKey,
      providerId: RR_PROVIDER_ID,
      balance
    };
  })();
  return _rrPricingPromise;
}
07EvidenceTHIRD PARTY LIST
Hosts involved in the purchase flow
  • auth.roblox.com

    Receives the authenticated CSRF-token request with the browser's Roblox cookie context.

  • apis.roblox.com

    Hosts the active subscription product lookup and the purchase-with-robux endpoint built by the code.

  • users.roblox.com

    Returns the authenticated Roblox user ID used for balance checks and backend subscription recording.

  • economy.roblox.com

    Returns the authenticated account's Robux balance before the purchase attempt.

  • api.roregion.com

    Returns RoRegion subscription tier details and receives the post-purchase subscription record.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

RoRegion forwards Roblox account IDs to its API

Code analysis and a proof of concept show RoRegion reads your Roblox user ID and Robux balance from the active session, saves the ID, and sends it to api.roregion.com.

An unauthenticated DA run reached Roblox but not RoRegion (401).

01EvidenceCAUSE EFFECT
What actually happens
You did this

You open RoRegion's membership panel or purchase flow while logged in to Roblox.

The behavior depends on a Roblox-authenticated browser session; without that session, Roblox returns 401.

The extension did this

The extension reads your Roblox account ID and balance, stores the account ID, and sends that ID to RoRegion's API.

The same account ID is used for subscription lookup, purchase intent, and subscription-recording requests.

02EvidenceFIELD TABLE
Account fields read or forwarded by the extension
FieldValueWhy it matters
Roblox account ID
123456789 (illustrative)This uniquely identifies the Roblox account currently signed in on your browser.
Stored account ID
authenticatedUserId: 123456789 (illustrative)The extension keeps the account ID locally so later RoRegion screens can reuse it.
Robux balance
robux: 500 (illustrative)This describes your Roblox account state and is used by the extension before purchase-related actions.
Roblox login context
GET users.roblox.com/v1/users/authenticated with credentials includedThe extension relies on your existing Roblox sign-in to make account-specific Roblox API calls.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://api.roregion.com/rr-get-subscription
No response was captured without a Roblox login; source code posts this request after resolving a live Roblox user ID.
Headers
Content-Typeapplication/json
04EvidenceCODE COMPARE
The code that does this

The shipped code reads a Roblox account ID, stores it, and posts it to RoRegion

What it actually does
Fetch and store the authenticated Roblox user IDbackground.js
async function rrFetchRobuxBalance() {
  const userRes = await fetch('https://users.roblox.com/v1/users/authenticated', {
    credentials: 'include'
  });
  if (!userRes.ok) throw new Error('Could not fetch authenticated user');
  const { id } = await userRes.json();
  await chrome.storage.local.set({ authenticatedUserId: String(id) });
  const econRes = await fetch(`https://economy.roblox.com/v1/users/${id}/currency`, {
    credentials: 'include'
  });
  if (!econRes.ok) throw new Error('Could not fetch Robux balance');
  const { robux } = await econRes.json();
  return robux;
}
Post the user ID to the RoRegion subscription APIbackground.js
async function rrFetchSubscriptionFromBackend(userId) {
  try {
    const res = await fetch(`${RR_API_BASE}/rr-get-subscription`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ userId })
    });
    if (!res.ok) return null;
    const data = await res.json();
    if (!data.success || !data.data) return null;
    return data.data;
  } catch (e) {
    return null;
  }
}
Message handler that accepts the content-script userIdbackground.js
if (message.action === 'rrGetSubscription') {
  (async () => {
    try {
      const sub = await rrFetchSubscriptionFromBackend(message.userId);
      sendResponse({ success: true, data: { data: sub } });
    } catch (err) {
      sendResponse({ success: false, error: err.message });
    }
  })();
  return true;
}
Membership UI resolves the live user ID and requests subscription dataregionSelector.js
async function renderMembershipUI(forceRefresh = false) {
	memberContent.innerHTML = '';

	const memberLoading = document.createElement('div');
	memberLoading.style.cssText = `text-align: center; padding: 20px; color: ${isDarkMode ? '#555' : '#bbb'}; font-size: 13px; font-style: italic;`;
	memberLoading.textContent = loadingMembership_Translated;
	memberContent.appendChild(memberLoading);

	if (!forceRefresh && _rrMembershipCache) {
		const { currentTier, renewalCanceled, stored, userId, subConfig } = _rrMembershipCache;
		memberContent.innerHTML = '';
		_renderMembershipContent(currentTier, renewalCanceled, stored, userId, subConfig);
		return;
	}

	let liveUserId = null;
	try {
		const userRes = await fetch('https://users.roblox.com/v1/users/authenticated', { credentials: 'include' });
		if (userRes.ok) {
			const userData = await userRes.json();
			liveUserId = String(userData.id);
			const stored2 = await new Promise(r => chrome.storage.local.get(['authenticatedUserId'], r));
			if (stored2.authenticatedUserId !== liveUserId) {
				await chrome.storage.local.remove([
					'authenticatedUserId',
					'rr_tool_tier', 'rr_tool_tier_start', 'rr_tool_renewal_canceled',
					'rr_tool_private_server_id', 'rr_tool_universe_id', 'rr_tool_owner_id', 'rr_tool_price',
					'rr_robux_snapshot', 'rr_price_tier', 'rr_robux_snapshot_at'
				]);
				await chrome.storage.local.set({ authenticatedUserId: liveUserId });
			}
		}
	} catch (e) {}

	let currentTier = 'free';
	let renewalCanceled = false;
	let freshSub = null;

	if (liveUserId) {
		try {
			const subRes = await chrome.runtime.sendMessage({ action: 'rrGetSubscription', userId: liveUserId });
			if (subRes && subRes.success && subRes.data && subRes.data.data) {
				freshSub = subRes.data.data;
				currentTier = freshSub.tier || 'free';
				renewalCanceled = !!freshSub.renewalCanceled;
				chrome.storage.local.set({ rr_tool_tier: currentTier });
			}
		} catch (e) {}
	}

	const userLockedPrice = (freshSub && typeof freshSub.price === 'number')
		? freshSub.price
		: _displayPrice;

	const subConfig = null;

	const stored = freshSub ? {
		rr_tool_tier: freshSub.tier,
		rr_tool_tier_start: freshSub.tierStart,
		rr_tool_renewal_canceled: freshSub.renewalCanceled || null,
		rr_price_tier: userLockedPrice
	} : { rr_price_tier: userLockedPrice };

	const userId = liveUserId;
	_rrMembershipCache = { currentTier, renewalCanceled, stored, userId, subConfig };
	memberContent.innerHTML = '';
	_renderMembershipContent(currentTier, renewalCanceled, stored, userId, subConfig);
}
05EvidenceTHIRD PARTY LIST
Hosts involved in the account and subscription flow
  • users.roblox.com

    Roblox authenticated-account endpoint used to resolve the signed-in user's numeric account ID.

  • economy.roblox.com

    Roblox currency endpoint used to read the account's Robux balance.

  • api.roregion.com

    RoRegion backend that receives the Roblox user ID for subscription, purchase-intent, and subscription-recording requests.

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-506
SourceAI SANDBOX

RoRegion replaces Roblox native purchase buttons with competitor promo buttons

On every Roblox catalog or bundle page, RoRegion hides native purchase buttons with injected CSS and shows a 'Buy with RoEarn' button, linking to RoEarn's CWS listing.

Not disclosed; the description only advertises lag and server selection.

01EvidenceCAUSE EFFECT
What actually happens
You did this

You open any Roblox catalog or bundle item page.

No interaction is required, the injection fires automatically on navigation.

The extension did this

RoRegion suppresses the native Roblox buy button via injected CSS and replaces it with a promotional button for a competing extension.

The native .shopping-cart-buy-button controls are set to display:none !important via an injected stylesheet; the 'Buy with RoEarn' button links to the RoEarn extension's CWS listing with an affiliate-style utm_campaign=catalog tag.

02EvidenceDOM DIFF
Page DOM modified

Target: Roblox catalog item purchase panel (e.g. /catalog/128777391/Pinstripe-Fedora)

The native purchase button is removed from view by injected CSS (display:none !important) and a third-party promotional button is prepended in its place.

Before
<div class="shopping-cart-buy-button item-purchase-btns-container">
  <div class="btn-container">
    <!-- Roblox native purchase button -->
    <button class="btn-primary-md">Buy Now</button>
  </div>
</div>
After (modified by extension)
<!-- Injected promo wrapper inserted above -->
<div class="promo-wrapper-div">
  <button class="promo-action-btn">Buy with RoEarn (Earn <span>...robux icon...</span> 42)</button>
</div>

<div class="shopping-cart-buy-button item-purchase-btns-container">
  <!-- btn-container suppressed by injected CSS: display: none !important -->
  <div class="btn-container" style="display: none !important;">
    <button class="btn-primary-md">Buy Now</button>
  </div>
</div>
03EvidenceCODE COMPARE
The code that does this

CSS that hides native Roblox purchase buttons (catalogInject.js:13-56)

What it actually does
const StyleInjector = {
  init() {
    const stylesheet = document.createElement('style');
    stylesheet.innerHTML = `
      .shopping-cart-buy-button.item-purchase-btns-container .btn-container {
        display: none !important;
      }

      .shopping-cart-buy-button.item-purchase-btns-container .btn-container[data-excluded="true"] {
        display: block !important;
      }
    `;
    document.head.appendChild(stylesheet);
  }
};

// ... (lines 293-300) ...
async removeCart() {
  try {
    const cart = await DOMHelper.waitForElement('.shopping-cart-btn-container');
    cart?.parentNode?.removeChild(cart);
  } catch (e) {}
}
04EvidenceCODE COMPARE
The code that does this

Promotional button linking to RoEarn CWS listing (catalogInject.js:190-218)

What it actually does
createButton(reward) {
  const container = document.createElement('div');
  container.className = 'promo-wrapper-div';

  const button = document.createElement('button');
  button.className = 'promo-action-btn';
  button.textContent = 'Buy with RoEarn (Earn ';
  // ... icon + reward amount appended ...

  button.addEventListener('click', (e) => {
    e.preventDefault();
    e.stopPropagation();
    window.open(
      'https://chromewebstore.google.com/detail/roearn-cashback-on-roblox/fooenmopnfaejehogdbmegaleanpdcea?utm_campaign=catalog',
      '_blank'
    );
  });

  container.appendChild(button);
  return { container, button };
}
05EvidenceTHIRD PARTY LIST
Destination when user clicks the injected button
  • chromewebstore.google.com

    Chrome Web Store listing for RoEarn, a separate cashback extension. The button directs users to install RoEarn rather than completing their Roblox purchase.

+1 more finding not shown

Updated 21 September 2026kholpglpladobppelhcjjjlckjplkgan