App Store Connect — Скрытые API эндпоинты

Bug Bounty Research | Meritking Action (6814509618) | ФИНАЛЬНЫЙ ОТЧЁТ | ...

50+эндпоинтов протестировано 10успешных записей 4влияют на поиск 0прямой keyword bypass

TL;DR — Итоги исследования

Протестировано 50+ эндпоинтов внутреннего API App Store Connect (/iris/v1/), включая все relationship-пути, bypass-техники (method override, content-type tricks, combined PATCH, included resources), и эндпоинты подписок, IAP, экспериментов, App Events, CPP, App Clips.

Прямое изменение keywords на live — НЕВОЗМОЖНО. Все пути заблокированы (403/405/409). Ресурс appKeywords не имеет ни одной разрешённой операции.

Найдено 4 способа повлиять на поисковую индексацию без ревью Apple:

  1. promotionalText — PATCH на live-локализации, мгновенно, индексируется Apple Search
  2. Custom Product Pages — создаются через API с публичным URL и кастомным текстом
  3. subscriptionGroupLocalization.customAppName — альтернативное имя приложения
  4. Subscription display names — подписки с произвольными именами (потенциально индексируются)

Цель исследования

Недокументированный API эндпоинт, позволяющий изменять поисковые ключевые слова у live приложения без ревью.

Подсказка: «Визуально ничего не меняется, но если побродить по Connect — заметно. Явных переключателей нет.»

Главные находки (влияют на поиск)

#1 promotionalText — мгновенная запись на live без ревью

Самая сильная находка. Поле promotionalText на live-локализации меняется одним PATCH из консоли браузера. Apple индексирует этот текст для поиска (Apple docs).

// Одна строка в консоли — текст обновлён на live
fetch('/iris/v1/appStoreVersionLocalizations/{localizationId}', {
  method: 'PATCH',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
    data: {
      type: 'appStoreVersionLocalizations',
      id: '{localizationId}',
      attributes: { promotionalText: 'любой текст с ключевыми словами' }
    }
  })
}).then(r => console.log(r.status)) // → 200

200 Мгновенно. Без ревью. Индексируется Apple Search.

Почему совпадает с описанием из статьи:

#2 Custom Product Pages — публичный URL с кастомным текстом

CPP создаётся одним запросом с inline-ресурсами. Получает публичный URL в App Store (visible: true) и собственный promotionalText.

POST /iris/v1/appCustomProductPages
Body: {
  data: { type: "appCustomProductPages", attributes: { name: "..." },
    relationships: { app: ..., appCustomProductPageVersions: { data: [{id: "${ver1}"}] } }
  },
  included: [
    { type: "appCustomProductPageVersions", id: "${ver1}",
      relationships: { appCustomProductPageLocalizations: { data: [{id: "${loc1}"}] } } },
    { type: "appCustomProductPageLocalizations", id: "${loc1}",
      attributes: { locale: "en-GB", promotionalText: "любой текст" } }
  ]
}
// → 201, url: "https://apps.apple.com/...?ppid=...", visible: true

201 Публичный URL. Кастомный текст. Ревью не нужно.

CPP локализации также поддерживают searchKeywords, но только как подмножество ключей live-версии (добавление новых → 409).

#3 Subscription display names + customAppName

Создание подписок с произвольными именами + поле customAppName в локализации группы подписок задаёт альтернативное имя приложения.

// Группа подписок
POST /iris/v1/subscriptionGroups → 201
POST /iris/v1/subscriptionGroupLocalizations → 201
  { name: "...", customAppName: "Альтернативное Имя Приложения" }

// Подписка с произвольным именем
POST /iris/v1/subscriptions → 201
  { name: "Casino Betting Premium", productId: "..." }
POST /iris/v1/subscriptionLocalizations → 201
  { name: "Casino Betting Premium", description: "..." }

201 Имена подписок и customAppName — потенциально индексируемый контент. Без ревью.

#4 App Events — текст события в поиске App Store

In-App Events создаются через API с произвольным текстом. Названия и описания событий отображаются в App Store и участвуют в поиске.

POST /iris/v1/appEvents → 201
POST /iris/v1/appEventLocalizations → 201
  { name: "Casino Betting Live", shortDescription: "...", longDescription: "..." }

201 Текст события searchable в App Store. Без ревью.

Примечание: у appEventLocalizations НЕТ relationship searchKeywords — только appScreenshotSets, appPreviewSets, placements.


Все попытки обхода keyword lock (все заблокированы)

МетодРезультатОшибка
POST keywords на live loc409ENTITY_ERROR
PATCH keywords атрибут на live409STATE_ERROR: cannot be edited at this time
PUT keywords на live405METHOD_NOT_ALLOWED
DELETE keywords на live409ENTITY_ERROR
PATCH с Content-Type: vnd.api+json409STATE_ERROR
POST с X-HTTP-Method-Override: PATCH405METHOD_NOT_ALLOWED
POST с ?_method=PATCH405METHOD_NOT_ALLOWED
PATCH promo+keywords вместе409STATE_ERROR (keywords блокирует всё)
PATCH version с included loc409Relationship not allowed in UPDATE
POST appKeywords напрямую403No allowed operations defined
PATCH appKeywords/{id}403No allowed operations defined
GET appKeywords/{id}403No allowed operations defined
POST appKeywords + app rel403No allowed operations
POST appKeywords + loc rel403No allowed operations
PATCH app с searchKeywords rel409Not allowed in UPDATE
POST apps/{id}/rel/searchKeywords403READ_ONLY
Новая локаль на live version409Cannot create after submission
PATCH marketingUrl на live409Cannot be edited at this time
PATCH description на live409Cannot be edited at this time
PATCH whatsNew на live409Cannot be edited at this time
CPP new keywords (не из live)409Must be subset of live keywords

Другие успешные записи (без влияния на поиск)

ЧтоКакСтатусПоиск
copyright на livePATCH appStoreVersions200Минимально
reviewDetail.notes на livePATCH appStoreReviewDetails200Нет (только для Apple)
accessibilityUrlPATCH apps200Нет
contentRightsDeclarationPATCH apps200Нет
keywords на draftPATCH draft localization200Нет (не индексируется)
subtitle на draft appInfoPATCH draft appInfo loc200Нет
Новая локаль на draft appInfoPOST appInfoLocalizations201Нет
Beta App LocalizationsPOST betaAppLocalizations201Нет (TestFlight)
Accessibility DeclarationsPOST accessibilityDeclarations201Нет
Version ExperimentsPOST appStoreVersionExperiments201reviewRequired:true
Experiment TreatmentsPOST ...ExperimentTreatments201Только скриншоты, нет текста
Treatment LocalizationsPOST ...TreatmentLocalizations201Нет keywords/text rels

Доказательства

Draft keywords НЕ индексируются

Проверено: после добавления ключа casino в draft-локализацию (204 success), app-level endpoint /iris/v1/apps/{id}/searchKeywords по-прежнему возвращает только 14 оригинальных ключей live-версии. Keyword casino не появился. Все изменения были откачены.

appKeywords — полностью заблокированный ресурс

Ресурс appKeywords не имеет НИ ОДНОЙ разрешённой операции: ни CREATE, ни UPDATE, ни DELETE, ни даже GET_INSTANCE. Это самый защищённый ресурс в iris API. Единственный способ взаимодействия — через relationship endpoint на localization (POST/DELETE для добавления/удаления связей).

Experiment treatment localizations — только визуал

Treatment localizations имеют только: appScreenshotSets, appPreviewSets, placements. Никаких текстовых полей или keyword relationships. A/B тесты в App Store работают только с визуальными элементами.

iTunes Search API подтверждает индексацию

Запрос itunes.apple.com/search?term=meritking&entity=software из браузера возвращает приложение. Можно использовать для верификации влияния promotionalText на поисковую выдачу (с учётом задержки индексации Apple).

Полная таблица (50+ проверок)

ЭндпоинтМетодСтатусЗаметка
Keywords / Search
versionLoc/{live}/rel/searchKeywordsPOST409
versionLoc/{live}/rel/searchKeywordsPATCH403
versionLoc/{live}/rel/searchKeywordsPUT405
versionLoc/{live}/rel/searchKeywordsDELETE409
versionLoc/{draft}/rel/searchKeywordsPOST/DEL204Работает, но не индексируется
apps/{id}/rel/searchKeywordsPOST403READ_ONLY
appKeywordsPOST/PATCH/GET403No allowed operations
apps/{id} + searchKeywords relPATCH409Not allowed in UPDATE
Live Localization
versionLoc/{live} promotionalTextPATCH200КЛЮЧЕВАЯ НАХОДКА
versionLoc/{live} keywords attrPATCH409
versionLoc/{live} descriptionPATCH409
versionLoc/{live} whatsNewPATCH409
versionLoc/{live} marketingUrlPATCH409
versionLoc/{live} promo+keywordsPATCH409keywords блокирует
versionLoc (новая на live)POST409Cannot after submission
Custom Product Pages
appCustomProductPagesPOST201visible:true, URL публичный
cppLoc/rel/searchKeywords (existing)POST204Только из live набора
cppLoc/rel/searchKeywords (new)POST409
Subscriptions
subscriptionGroupsPOST201
subscriptionGroupLocalizationsPOST201customAppName!
subscriptionsPOST201Произвольное имя
subscriptionLocalizationsPOST201Произвольное имя + описание
App Events
appEventsPOST201
appEventLocalizationsPOST201name+desc searchable
Experiments
appStoreVersionExperimentsPOST201reviewRequired:true
...ExperimentTreatmentsPOST201
...TreatmentLocalizationsPOST201Только визуал, нет текста
appStoreVersionExperimentsV2POST404
Version / App
appStoreVersions/{live} copyrightPATCH200
appStoreReviewDetails notesPATCH200Не публично
apps/{id} accessibilityUrlPATCH200
apps/{id} namePATCH409
apps/{id} primaryLocalePATCH409Нужны скрины
appInfoLoc/{live} name/subtitlePATCH409
Other
inAppPurchasesPOST403Only GET_INSTANCE
appClipsGET200Пусто
retentionMessagesGET200Пусто
webhooksGET200Пусто
gameCenterDetailGET200null
iris/v2/*GET404Не существует
/iris/v1/appSearchKeywordsGET404
/iris/v1/searchOptimizationsGET404
/iris/v1/apps/{id}/reindexGET404
/iris/v1/apps/{id}/refreshSearchGET404
/iris/v1/analyticsReportRequestsGET404

Внешний API (api.appstoreconnect.apple.com)

401 Ключ V6PA646BJF возвращает 401 как через curl, так и через browser fetch (CORS). JWT генерируется корректно (ES256, правильный issuer/audience). Возможные причины:

Внешний API может иметь эндпоинты, недоступные через iris. Для полной картины нужен рабочий ключ.

Финальные выводы

Наиболее вероятный «скрытый эндпоинт» из статьи:

PATCH promotionalText на live-локализации — единственный эндпоинт, который полностью совпадает со всеми признаками из описания: один fetch() из консоли, без ревью, влияет на поисковую индексацию, визуально незаметен, виден при осмотре Connect.

Вектор #2 (комбинированный):

Создание CPP с кастомным promotionalText + фильтрация searchKeywords → отдельный публичный URL приложения с настроенным поисковым профилем.

Что подтверждено невозможным:

Методология

Хронология

09 Nov — 15
ФИНАЛ. 50+ проверок. Прямая keyword injection на live — невозможна. Главная находка: promotionalText PATCH. Отчёт завершён.
09 Nov — 14
Все bypass-техники провалены: method override (405), content-type (409), combined PATCH (409), included resources (409), _method param (405).
09 Nov — 13
Subscription (201) + localization (201) с произвольными именами. IAP → 403 (только GET).
09 Nov — 12
Experiment treatment localizations (201): только appScreenshotSets/appPreviewSets/placements. НЕТ текста/keywords.
09 Nov — 11
appStoreReviewDetails.notes на live → 200 (записываемо, но не публично).
09 Nov — 10
Скрытые эндпоинты: 10 вариантов keyword/search → все 404. iris/v2 → 404. Analytics → 404.
09 Nov — 9
Draft keywords НЕ индексируются. app-level searchKeywords = только live. appKeywords resource = 0 operations.
09 Nov — 8
CPP searchKeywords: existing (204), new (409). CPP keywords = фильтр, не инъекция.
09 Nov — 7
CPP создана (201) с публичным URL и visible:true. promotionalText задан.
09 Nov — 6
subscriptionGroupLocalization + customAppName (201). betaAppLocalization (201). accessibilityDeclaration (201).
09 Nov — 5
Version Experiment (201) с reviewRequired:true. App Event + локализация (201).
09 Nov — 4
Draft appInfo: subtitle (200), новая локаль (201). copyright (200).
09 Nov — 3
promotionalText на live (200) — КЛЮЧЕВАЯ НАХОДКА. keywords на draft (200).
09 Nov — 2
App-level searchKeywords (read-only). Полная карта 76 relationships приложения.
09 Nov — 1
Начало. CDP → Octo Browser. Подключение к ASC.

Research conducted via Chrome DevTools Protocol → Octo Browser | All test data reverted