我们正在公开一个API,合作伙伴只能在他们向我们注册的域上使用。它的内容部分是公开的(但最好仅在我们已知的域中显示),但大部分对我们的用户是私有的。所以:
该API密钥的确对所有人可见,我们不会以任何其他方式对合作伙伴进行身份验证,并且不需要REFERER。仍然是安全的:
当我们get-csrf-token.js?apiKey=abc123
被要求时:
查找关键abc123
的数据库并获取该密钥的有效域的列表。
查找CSRF验证cookie。如果不存在,则生成一个安全的随机值,并将其放入仅HTTP的会话cookie中。如果cookie存在,则获取现有的随机值。
从API密钥创建CSRF令牌,从cookie创建随机值,并对其签名。(我们不是在服务器上保留令牌列表,而是对值进行签名。两个值都可以在签名的令牌中读取,这很好。)
将响应设置为不缓存,添加cookie,然后返回如下脚本:
var apiConfig = apiConfig || {};
if(document.domain === 'expected-domain.com'
|| document.domain === 'www.expected-domain.com') {
apiConfig.csrfToken = 'API key, random value, signature';
// Invoke a callback if the partner wants us to
if(typeof apiConfig.fnInit !== 'undefined') {
apiConfig.fnInit();
}
} else {
alert('This site is not authorised for this API key.');
}
笔记:
上面的内容不会阻止服务器端脚本伪造请求,而只是确保域匹配 ,如果浏览器请求。
JavaScript的相同原始策略可确保浏览器无法使用XHR(Ajax)加载并检查JavaScript源。相反,常规浏览器只能使用<script src="https://our-api.com/get-csrf-token.js?apiKey=abc123">
(或动态等效项)加载它,然后运行代码。当然,你的服务器应该不支持跨来源资源共享,也为JSONP生成的JavaScript。
浏览器脚本可以document.domain
在加载上述脚本之前更改的值。但是相同的原始策略仅允许通过删除前缀来缩短域,例如重写subdomain.example.com
为just example.com
或myblog.wordpress.com
to wordpress.com
,或者在某些浏览器中甚至重写bbc.co.uk
为co.uk
。
如果使用某些服务器端脚本提取了JavaScript文件,则服务器还将获取cookie。但是,第三方服务器无法使用户的浏览器将该Cookie关联到我们的域。因此,已经使用服务器端脚本获取的CSRF令牌和验证cookie只能由后续的服务器端调用使用,而不能在浏览器中使用。但是,这样的服务器端调用将永远不会包含用户cookie,因此只能获取公共数据。这是服务器端脚本可以直接从合作伙伴的网站抓取的数据。
用户登录时,可以根据自己的喜好设置一些用户cookie。(在请求JavaScript之前,用户可能已经登录。)
到服务器的所有后续API请求(包括GET和JSONP请求)必须包括CSRF令牌,CSRF验证cookie和用户cookie(如果已登录)。服务器现在可以确定是否信任该请求:
有效的CSRF令牌的存在可确保从预期的域中加载JavaScript, ,如果装载由浏览器。
CSRF令牌的存在 没有验证cookie表示伪造。
CSRF令牌和CSRF验证cookie的存在不能确保任何事情:这可以是伪造的服务器端请求,也可以是来自浏览器的有效请求。(这可能不是来自不受支持域的浏览器发出的请求。)
用户cookie的存在可确保用户已登录,但不能确保该用户是给定合作伙伴的成员,也不能确保该用户正在查看正确的网站。
没有 CSRF验证cookie 的用户cookie的存在表示伪造。
用户cookie的存在可确保通过浏览器发出当前请求。(假设用户不会在未知的网站上输入其凭据,并假设我们不在乎用户使用自己的凭据来发出服务器端请求。)如果我们也有CSRF验证Cookie,则该CSRF验证Cookie为也使用浏览器接收。接下来,如果我们也一个带有有效签名的CSRF令牌,并且CSRF验证Cookie中的随机数与该CSRF令牌中的随机数匹配,那么在设置CSRF Cookie的同一请求中,该令牌的JavaScript也会收到,因此也使用浏览器。这也意味着上述JavaScript代码是在设置令牌之前执行的,并且那时该域对于给定的API密钥有效。
因此:服务器现在可以安全地使用已签名令牌中的API密钥。
如果服务器在任何时候都不信任该请求,则返回403 Forbidden。小部件可以通过向用户显示警告来对此做出响应。
不需要签署CSRF验证Cookie,因为我们正在将其与已签署的CSRF令牌进行比较。不对cookie进行签名会使每个HTTP请求更短,并且服务器验证速度会更快。
生成的CSRF令牌无限期有效,但仅与验证cookie结合使用,因此有效,直到关闭浏览器为止。
我们可以限制令牌签名的寿命。当用户注销时,我们可以删除CSRF验证cookie,以满足OWASP的建议。为了避免在多个合作伙伴之间共享每个用户的随机数,可以将API密钥添加到cookie名称中。但是即使这样,当请求新令牌时,也无法轻易刷新CSRF验证cookie,因为用户可能会在多个窗口中浏览同一站点,共享一个cookie(刷新时,它将在所有窗口中更新,此后,其他窗口中的JavaScript令牌将不再与该单个Cookie匹配)。
对于使用OAuth的用户,另请参阅OAuth和客户端小部件,我从中获得了JavaScript想法。对于服务器端使用的API,我们不能依靠JavaScript代码来限制域,我们使用的是私钥而不是公共API密钥。