🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Лимит

Има такова ограничение в LinkedIn — Лимит на търговска употреба. Доста вероятно е, че вие, като мен до неотдавна, никога не сте се сблъсквали с него и не сте чували за него.

🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Същността на лимита е, че ако използвате търсенето на хора извън вашите контакти твърде често (нямате точни метрики, алгоритъмът решава на базата на вашите действия — колко често и много търсите, добавяте хора), то резултатът от търсенето ще бъде ограничен до три профила вместо 1000 (по подразбиране 100 страници по 10 профила на страница). Лимитът се нулира в началото на всеки месец. Разбира се, премиум акаунтите нямат такова ограничение.

Но неотдавна, за един личен проект, започнах да експериментирам много с търсенето в LinkedIn и изведнъж получих това ограничение. Разбира се, не ми хареса, защото не го използвах за търговски цели, затова първата ми мисъл беше да проуча ограничението и да опитам да го заобиколя.

[Важно уточнение — материалите в статията са представени единствено за ознакомителни и обучителни цели. Авторът не подканя тяхното използване за търговски цели.]

Изучаваме проблема

Имаме: вместо десет профила с пагинация, търсенето дава само три, след които идва блок с „препоръка“ за премиум акаунт, а по-долу следват размазани и некликаеми профили.

Веднага ръката се търси в конзолата на разработчика, за да се видят тези скрити профили — може би можем да премахнем някакви стилове, които правят размазването, или да извлечем информация от блока в разметката. Но, напълно очаквано, тези профили са само временни изображения и не съхраняват никаква информация.

🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Добре, сега да видим в таба Network и да проверим наистина ли се задейства алтернативното извеждане на резултатите от търсенето, което връща само три профила. Намираме ни интересния запит за “/api/search/blended” и виждаме отговора.

🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Профилите идват в масива `included`, но има 15 entidades в него. В този случай, първите три от тях са обекти с допълнителна информация, всеки обект съдържа информация за конкретен профил (например, дали профилът е премиум).

🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Следващите 12 са реални профили – резултати от търсенето, от които ще ни покажат само трите. Както може да се предполага, показва само тези, за които идва допълнителна информация (първите три обекта). Например, ако вземем отговора от профила без лимит, ще получим 28 единици – 10 обекта с допълнителна информация и 18 профила.

Отговор за профил без лимит🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster
🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Защо идват повече от 10 профила, когато се изискват точно 10, и те не участват в показването, дори на следващата страница няма да ги има – все още не знам. Ако анализираме URL на заявката, можем да видим, че count=10 (колко профила да се върнат в отговора, максимум 49).

🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Ще се радвам на всякакви коментари по този въпрос.

Експериментираме

Добре, най-важното, което сега знаем – профилите идват в отговора повече, отколкото ни показват. Значи можем да извлечем повече данни, независимо от лимита. Нека опитаме да извлечем API сами, директно от консолата, с помощта на fetch.

🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Очаквано, получаваме грешка, 403. Това е свързано с безопасността, тук не изпращаме CSRF токен (CSRF на Уикипедия. В две думи – към всяка заявка се добавя уникален токен, който се проверява на сървъра за автентичност).

🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Може да бъде копиран от всяка друга успешна заявка или от бисквитки, където се съхранява в полето ‘JSESSIONID’.

Къде да намеря токенаЗаглавие на друга заявка:

🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Или от бисквитките, направо през консолата:

🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Опитваме отново, този път предаваме във fetch настройките, в които посочваме като параметър в заглавката нашия csrf-токен.

🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Успех, получаваме всичките 10 профила. :tada:

Заради разликите в заглавките, структурата на отговора малко се различава от това, което идва в оригиналната заявка. Може да получим същата структура, ако добавим ‘Accept: ‘application/vnd.linkedin.normalized+json+2.1’, в нашия обект, до CSRF токена.
Пример за отговор с добавено заглавие🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Повече за заглавието Accept

Какво следва?

След това можем да редактираме (ръчно или автоматизирано) параметъра `start`, който указва индекса, от който ще ни дадат 10 профила (по подразбиране = 0) от всичките резултати от търсенето. С други думи, увеличавайки го с 10 след всяка заявка, получаваме обикновена странична лента, по 10 профила на куп.

В този етап разполагах с достатъчно данни и свобода, за да продължа работа по личния проект. Но би било грешка да не опитам да визуализирам тези данни на място, след като ги имам. Няма да навлизам в Ember, който се използва на фронтенда. На сайта беше включен jQuery и, като си припомня знанията за основния синтаксис, може за няколко минути да създам следното.

Код на jQuery

/* рендер блока, принимаем данные профиля и вставляем блок в список профилей используя эти данные */
const  createProfileBlock = ({ headline, publicIdentifier, subline, title }) => {
    $('.search-results__list').append(
        `<li class="search-result search-result__occluded-item ember-view">
            <div class="search-entity search-result search-result--person search-result--occlusion-enabled ember-view">
                <div class="search-result__wrapper">
                    <div class="search-result__image-wrapper">
                        <a class="search-result__result-link ember-view" href="/bg/in/${publicIdentifier}/">
                            <figure class="search-result__image">
                                <div class="ivm-image-view-model ember-view">
                                    <img class="lazy-image ivm-view-attr__img--centered EntityPhoto-circle-4  presence-entity__image EntityPhoto-circle-4 loaded" src="http://www.userlogos.org/files/logos/give/Habrahabr3.png" />
                                </div>
                            </figure>
                        </a>
                    </div>
                    
                    <div class="search-result__info pt3 pb4 ph0">
                        <a class="search-result__result-link ember-view" href="/bg/in/${publicIdentifier}/">
                            <h3 class="actor-name-with-distance search-result__title single-line-truncate ember-view">
                                ${title.text}
                            </h3>
                        </a>

                        <p class="subline-level-1 t-14 t-black t-normal search-result__truncate">${headline.text}</p>

                        <p class="subline-level-2 t-12 t-black--light t-normal search-result__truncate">${subline.text}</p>
                    </div>
                </div>
            </div>
        <li>`
 );
};

// Извличаме API, получаваме данни и рендерим профилите
const fetchProfiles = () =&gt; {
 // токен
 const csrf = 'ajax:9082932176494192209';
 
 // обект с настройки на запитването, предаваме токена
 const settings = { headers: { 'csrf-token': csrf } }

 // URL на запитването, с динамичен индекс за начало в края
 const url = `https://www.linkedin.com/voyager/api/search/blended?count=10&amp;filters=List(geoRegion-jp0,network-S,resultType-PEOPLE)&amp;origin=FACETED_SEARCH&amp;q=all&amp;queryContext=List(spellCorrectionEnabled-true,relatedSearchesEnabled-true)&amp;start=${nextItemIndex}`; 
 /* правим запитване, за всеки профил в отговора извикваме рендер блок, и след това увеличаваме стартовия индекс с 10 */
 fetch(url, settings).then(response =&gt; response.json()).then(data =&gt; {
 data.elements[0].elements.forEach(createProfileBlock);
 nextItemIndex += 10;
});
};


// премахваме всички профили от списъка
$('.search-results__list').find('li').remove();
// вмъкваме бутон за зареждане на профили
$('.search-results__list').after('<button id="load-more">Зареди повече</button>');
// добавяме функционалност на бутона
$('#load-more').addClass('artdeco-button').on('click', fetchProfiles);

// задаваме по подразбиране индекс на профила за запитването
window.nextItemIndex = 0;

Ако изпълните това директно в конзолата на страницата с търсене, то ще добави бутон, който зарежда 10 нови профила при всяко натискане и ще ги визуализира в списък. Разбира се, преди това трябва да смените токена и URL адреса на необходимите. Профилният блок ще съдържа име, длъжност, местоположение, линк към профила и изображение-заглушка.

🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Заключение

По този начин, с минимални усилия, успяхме да намерим уязвимост и да възстановим търсенето без ограничения. Достатъчно беше да анализираме данните и техния маршрут, да се загледаме в самия запит.

Не мога да кажа, че това е сериозен проблем за LinkedIn, тъй като не представлява заплаха. Най-много, това е загубена печалба заради подобни "заобиколки", позволяващи да не се плаща за премиум. Възможно е, такъв отговор от сървъра да е необходим за коректната работа на други части от сайта, или просто е мързел на разработчиците, недостатък на ресурсите, който не позволява да се направи добре. (Ограничението е въведено през януари 2015 година, преди това лимит нямаше).

P.S.

Естествено, кодът на jQuery е доста примитивен пример за възможностите. В момента съм създал разширение за браузър за собствените си нужди. То добавя контролните бутони и визуализира пълни профили с изображения, бутон за покана и общи свързвания. Плюс динамично събира филтри за местоположения, компании и други, извлича токена от бисквитките. Така че не е нужно да се хардкодва нищо. А също така добавя допълнителни полета за настройки, а-ля "колко профила да се заявят наведнъж, до 49".

🥇GitHub е сред ответниците по делото за изтичането на потребителската база на Capital One | ProHoster

Все още работя над това разширение и планирам да го публикувам с отворен достъп. Пишете, ако ви интересува.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster