Limite
Il existe une telle restriction sur LinkedIn — . Il est très probable que vous, tout comme moi jusqu'à récemment, n'en ayez jamais entendu parler ni rencontré.

L'idée du limite est que si vous utilisez la recherche de personnes en dehors de vos contacts trop souvent (il n'y a pas de métriques précises, cela dépend de l'algorithme, basé sur vos actions — à quelle fréquence et combien vous avez cherché et ajouté des personnes), alors le résultat de recherche sera limité à trois profils, au lieu de 1000 (par défaut 100 pages, avec 10 profils par page). La limite est réinitialisée au début de chaque mois. Évidemment, .
Mais récemment, pour un petit projet, j'ai commencé à jouer beaucoup avec la recherche sur LinkedIn et j'ai soudainement rencontré cette limitation. Naturellement, ça ne m'a pas beaucoup plu, car je ne l'utilisais pas à des fins commerciales, donc ma première pensée a été d'étudier cette restriction et d'essayer de la contourner.
[Une précision importante — les matériaux dans l'article sont présentés uniquement à des fins d'information et d'apprentissage. L'auteur n'encourage pas leur utilisation à des fins commerciales.]
Nous examinons le problème
Nous avons : au lieu de dix profils avec pagination, la recherche ne renvoie que trois, après quoi un bloc avec "recommandation" de compte premium apparaît, suivi de profils flous et non cliquables.
Automatiquement, ma main se dirige vers la console du développeur pour voir ces profils cachés — peut-être pouvons-nous supprimer certains styles qui créent du flou, ou extraire des informations du bloc dans le balisage. Mais, comme prévu, ces profils ne sont que et ne contiennent aucune information.

Bien, maintenant regardons l'onglet Réseau et vérifions si une alternative de résultats de recherche est réellement déclenchée, ne renvoyant que trois profils. Nous trouvons la requête d'intérêt vers "/api/search/blended" et voyons la réponse.

Les profils viennent dans le tableau `included`, mais il y a en tout 15 entités. Dans ce cas, les trois premiers sont des objets avec des informations supplémentaires, chaque objet contenant des informations sur un profil spécifique (par exemple, si le profil est premium).

Les 12 suivants sont de vrais profils - des résultats de recherche, dont seuls trois nous seront présentés. Comme on peut s'en douter, seuls ceux pour lesquels des informations supplémentaires arrivent sont affichés (les trois premiers objets). Par exemple, si on prend la réponse d'un profil sans limite, cela donnera 28 entités - 10 objets avec des informations supplémentaires et 18 profils.
Réponse pour un profil sans limite

Pourquoi y a-t-il plus de profils qui arrivent que les 10 spécifiés, alors qu'ils ne participent pas du tout à l'affichage, même sur la page suivante ils ne seront pas là - je ne sais pas encore. En analysant l'URL de la requête, on peut voir que count=10 (combien de profils retourner dans la réponse, maximum 49).
Je serais heureux de recevoir des commentaires à ce sujet.
Expérimentons
Eh bien, la chose la plus importante que nous savons maintenant avec certitude - plus de profils arrivent dans la réponse que ceux qui nous sont montrés. Cela signifie que nous pouvons extraire plus de données, malgré la limite. Essayons de solliciter l'API nous-mêmes, directement depuis la console, via fetch.

Comme prévu, nous obtenons une erreur, 403. Cela est dû à des raisons de sécurité, ici nous n'envoyons pas de jeton CSRF (). En d'autres termes, un jeton unique est ajouté à chaque requête, qui est vérifié sur le serveur pour son authenticité).

On peut le copier à partir de n'importe quelle autre requête réussie ou à partir des cookies, où il est stocké dans le champ ‘JSESSIONID’.
Où trouver le jetonEn-tête d'une autre requête :

Ou depuis les cookies, directement via la console :

Essayons encore une fois, cette fois en passant dans fetch les paramètres, où nous indiquons notre csrf-token dans l'en-tête.

Succès, nous recevons tous les 10 profils. :tada:
En raison des différences d'en-têtes, la structure de la réponse est légèrement différente de celle de la requête originale. On peut obtenir la même structure en ajoutant 'Accept: 'application/vnd.linkedin.normalized+json+2.1' à notre objet, à côté du jeton csrf.
Exemple de réponse avec en-tête ajouté
Et après ?
Ensuite, on peut modifier (manuellement ou automatiser) le paramètre `start`, indiquant l'index à partir duquel on nous remettra 10 profils (par défaut = 0) de l'ensemble des résultats de recherche. En d'autres termes, en l'incrémentant de 10 après chaque requête, nous obtenons une pagination classique, avec 10 profils à la fois.
À ce stade, j'avais suffisamment de données et de liberté pour continuer à travailler sur mon projet personnel. Mais il serait dommage de ne pas essayer d'afficher ces données directement sur place, puisque je les ai sous la main. Nous n'allons pas toucher à Ember, qui est utilisé sur le front. Le site avait jQuery d'installé, et en fouillant dans mes connaissances de base du syntaxe, je peux créer ce qui suit en quelques minutes.
Code 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="/fr/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="/fr/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>`
);
};
// appel de l'API, récupération des données et rendu des profils
const fetchProfiles = () => {
// jeton
const csrf = 'ajax:9082932176494192209';
// objet de configuration de la requête, en passant le jeton
const settings = { headers: { 'csrf-token': csrf } }
// url de la requête, avec un index de départ dynamique à la fin
const url = `https://www.linkedin.com/voyager/api/search/blended?count=10&filters=List(geoRegion-jp0,network-S,resultType-PEOPLE)&origin=FACETED_SEARCH&q=all&queryContext=List(spellCorrectionEnabled-true,relatedSearchesEnabled-true)&start=${nextItemIndex}`;
/* faisons la requête, pour chaque profil dans la réponse, appelons le rendu du bloc, puis incrémentons l'index de départ de 10 */
fetch(url, settings).then(response => response.json()).then(data => {
data.elements[0].elements.forEach(createProfileBlock);
nextItemIndex += 10;
});
};
// supprimons tous les profils de la liste
$('.search-results__list').find('li').remove();
// insérons le bouton de chargement des profils
$('.search-results__list').after('<button id="load-more">Charger plus</button>');
// ajoutons des fonctionnalités au bouton
$('#load-more').addClass('artdeco-button').on('click', fetchProfiles);
// définissons par défaut l'index de profil pour la requête
window.nextItemIndex = 0;
Si vous le faites directement dans la console sur la page de recherche, cela ajoutera un bouton qui charge 10 nouveaux profils à chaque clic, et les affiche sous forme de liste. Bien sûr, changez le token et l'URL en consequence. Le bloc de profil contiendra le nom, le poste, la localisation, le lien vers le profil et une image de remplacement.

Conclusion
Ainsi, avec un minimum d'efforts, nous avons pu identifier une vulnérabilité et retrouver une recherche sans restrictions. Il suffisait d'analyser les données et leur chemin, ainsi que de jeter un œil à la requête elle-même.
Je ne peux pas dire que cela représente un problème sérieux pour LinkedIn, car cela ne constitue aucune menace. Au maximum, c'est un manque à gagner à cause de telles "dérobades," permettant de ne pas payer pour un abonnement premium. Il se peut que cette réponse du serveur soit nécessaire au bon fonctionnement d'autres parties du site, ou que ce soit tout simplement de la paresse de la part des développeurs, un manque de ressources qui les empêche de bien faire. (Cette restriction a été introduite en janvier 2015, avant cela il n'y avait pas de limite).
P.S.
Bien sûr, le code jQuery est un exemple assez primitif des possibilités. En ce moment, j'ai créé une extension pour mon navigateur selon mes besoins. Elle ajoute des boutons de contrôle et affiche des profils complets avec des images, un bouton d'invitation et des connexions communes. De plus, elle collecte dynamiquement des filtres de localisation, d'entreprises, etc., et extrait le token des cookies. Donc, il n'est plus nécessaire de hardcoder quoi que ce soit. Et elle ajoute des champs supplémentaires dans les paramètres, comme "combien de profils demander à la fois, jusqu'à 49".

Je travaille toujours sur cette extension et j'ai l'intention de la publier en open source. N'hésitez pas à me contacter si cela vous intéresse.
Source : habr.com


