因此,第一层防御措施是避免在url中使用参数进行敏感调用,因此我将POST请求与请求正文中的所有参数一起使用,因为这种类型的请求无法通过只需将URL复制粘贴到浏览器或任何其他工具中,这样他们就需要更多的精力和知识来执行,也就是攻击者树上的果实更多.另一个原因是GET请求最终出现在服务器的日志中,因此可能被意外暴露并易于重播.针对API调用重播攻击是的,即使您没有公开的文档,他们也可以了解您的API的工作方式,他们只需要借助用于移动应用程序和Web应用程序的任何开源工具的帮助,对它进行认真的设计./p>是的,这还不够,因为此临时令牌可以通过MitM攻击被盗,就像文章在中间攻击中与一名男子偷走那个Api钥匙:因此,在执行MitM攻击以窃取令牌后,很容易使用curl,Postman或任何其他类似的工具向API服务器发出请求,就像您是真正的谁和什么.缓解重播攻击对现有安全防护的改进这种方法虽然不错,但还不足以引起您的注意,但您可以通过使此临时令牌仅一次使用来改进它(如果尚未完成).另一项重要的防御措施是即使具有新的临时令牌,也不允许顺序重复相同金额和相同收件人(from_account,to_account)的请求.也不允许将来自同一来源的请求快速发送出去,特别是如果这些请求是来自人机交互的话.这种措施本身并不能完全解决问题,但会在洋葱上添加更多的层.使用HMAC一次性令牌为了尝试帮助服务器对谁和什么发出请求充满信心,您可以使用密钥散列消息验证码(HMAC),旨在防止劫持和篡改,并且根据Wikipedia:因此,您可以让客户端使用请求网址,用户身份验证令牌,您的临时令牌以及也应出现在请求标头中的时间戳来创建HMAC令牌.然后,服务器将从请求中获取相同的数据并执行其自己的HMAC令牌计算,并且仅在其自身的结果与请求中的HMAC令牌标头匹配的情况下,才继续进行请求.有关实际操作的实际示例,您可以阅读这个博客系列涉及移动应用程序上下文中的API保护技术,该功能还具有模拟移动应用程序的网络应用程序.所以您可以看到此处移动应用如何计算HMAC,以及此处 Api服务器如何计算和验证它.但是您也可以看到此处 Web应用程序如何伪造HMAC令牌以使API服务器认为请求确实来自谁和什么希望来自移动应用程序. 移动应用代码:: /** * Compute an API request HMAC using the given request URL and authorization request header value. * * @param context the application context * @param url the request URL * @param authHeaderValue the value of the authorization request header * @return the request HMAC */ private fun calculateAPIRequestHMAC(url: URL, authHeaderValue: String): String { val secret = HMAC_SECRET var keySpec: SecretKeySpec // Configure the request HMAC based on the demo stage when (currentDemoStage) { DemoStage.API_KEY_PROTECTION, DemoStage.APPROOV_APP_AUTH_PROTECTION -> { throw IllegalStateException("calculateAPIRequestHMAC() not used in this demo stage") } DemoStage.HMAC_STATIC_SECRET_PROTECTION -> { // Just use the static secret to initialise the key spec for this demo stage keySpec = SecretKeySpec(Base64.decode(secret, Base64.DEFAULT), "HmacSHA256") Log.i(TAG, "CALCULATE STATIC HMAC") } DemoStage.HMAC_DYNAMIC_SECRET_PROTECTION -> { Log.i(TAG, "CALCULATE DYNAMIC HMAC") // Obfuscate the static secret to produce a dynamic secret to initialise the key // spec for this demo stage val obfuscatedSecretData = Base64.decode(secret, Base64.DEFAULT) val shipFastAPIKeyData = loadShipFastAPIKey().toByteArray(Charsets.UTF_8) for (i in 0 until minOf(obfuscatedSecretData.size, shipFastAPIKeyData.size)) { obfuscatedSecretData[i] = (obfuscatedSecretData[i].toInt() xor shipFastAPIKeyData[i].toInt()).toByte() } val obfuscatedSecret = Base64.encode(obfuscatedSecretData, Base64.DEFAULT) keySpec = SecretKeySpec(Base64.decode(obfuscatedSecret, Base64.DEFAULT), "HmacSHA256") } } Log.i(TAG, "protocol: ${url.protocol}") Log.i(TAG, "host: ${url.host}") Log.i(TAG, "path: ${url.path}") Log.i(TAG, "Authentication: $authHeaderValue") // Compute the request HMAC using the HMAC SHA-256 algorithm val hmac = Mac.getInstance("HmacSHA256") hmac.init(keySpec) hmac.update(url.protocol.toByteArray(Charsets.UTF_8)) hmac.update(url.host.toByteArray(Charsets.UTF_8)) hmac.update(url.path.toByteArray(Charsets.UTF_8)) hmac.update(authHeaderValue.toByteArray(Charsets.UTF_8)) return hmac.doFinal().toHex() } API服务器代码: if (DEMO.CURRENT_STAGE == DEMO.STAGES.HMAC_STATIC_SECRET_PROTECTION) { // Just use the static secret during HMAC verification for this demo stage hmac = crypto.createHmac('sha256', base64_decoded_hmac_secret) log.info('---> VALIDATING STATIC HMAC <---') } else if (DEMO.CURRENT_STAGE == DEMO.STAGES.HMAC_DYNAMIC_SECRET_PROTECTION) { log.info('---> VALIDATING DYNAMIC HMAC <---') // Obfuscate the static secret to produce a dynamic secret to use during HMAC // verification for this demo stage let obfuscatedSecretData = base64_decoded_hmac_secret let shipFastAPIKeyData = new Buffer(config.SHIPFAST_API_KEY) for (let i = 0; i < Math.min(obfuscatedSecretData.length, shipFastAPIKeyData.length); i++) { obfuscatedSecretData[i] ^= shipFastAPIKeyData[i] } let obfuscatedSecret = new Buffer(obfuscatedSecretData).toString('base64') hmac = crypto.createHmac('sha256', Buffer.from(obfuscatedSecret, 'base64')) } let requestProtocol if (config.SHIPFAST_SERVER_BEHIND_PROXY) { requestProtocol = req.get(config.SHIPFAST_REQUEST_PROXY_PROTOCOL_HEADER) } else { requestProtocol = req.protocol } log.info("protocol: " + requestProtocol) log.info("host: " + req.hostname) log.info("originalUrl: " + req.originalUrl) log.info("Authorization: " + req.get('Authorization')) // Compute the request HMAC using the HMAC SHA-256 algorithm hmac.update(requestProtocol) hmac.update(req.hostname) hmac.update(req.originalUrl) hmac.update(req.get('Authorization')) let ourShipFastHMAC = hmac.digest('hex') // Check to see if our HMAC matches the one sent in the request header // and send an error response if it doesn't if (ourShipFastHMAC != requestShipFastHMAC) { log.error("\tShipFast HMAC invalid: received " + requestShipFastHMAC + " but should be " + ourShipFastHMAC) res.status(403).send() return } log.success("\nValid HMAC.") Web APP代码: function computeHMAC(url, idToken) { if (currentDemoStage == DEMO_STAGE.HMAC_STATIC_SECRET_PROTECTION || currentDemoStage == DEMO_STAGE.HMAC_DYNAMIC_SECRET_PROTECTION) { var hmacSecret if (currentDemoStage == DEMO_STAGE.HMAC_STATIC_SECRET_PROTECTION) { // Just use the static secret in the HMAC for this demo stage hmacSecret = HMAC_SECRET } else if (currentDemoStage == DEMO_STAGE.HMAC_DYNAMIC_SECRET_PROTECTION) { // Obfuscate the static secret to produce a dynamic secret to // use in the HMAC for this demo stage var staticSecret = HMAC_SECRET var dynamicSecret = CryptoJS.enc.Base64.parse(staticSecret) var shipFastAPIKey = CryptoJS.enc.Utf8.parse($("#shipfast-api-key-input").val()) for (var i = 0; i < Math.min(dynamicSecret.words.length, shipFastAPIKey.words.length); i++) { dynamicSecret.words[i] ^= shipFastAPIKey.words[i] } dynamicSecret = CryptoJS.enc.Base64.stringify(dynamicSecret) hmacSecret = dynamicSecret } if (hmacSecret) { var parser = document.createElement('a') parser.href = url var msg = parser.protocol.substring(0, parser.protocol.length - 1) + parser.hostname + parser.pathname + idToken var hmac = CryptoJS.HmacSHA256(msg, CryptoJS.enc.Base64.parse(hmacSecret)).toString(CryptoJS.enc.Hex) return hmac } } return null}如您所见,跨移动应用程序计算HMAC令牌的方式,Api服务器和Web应用程序在逻辑语义上是相同的,因此产生相同的HMAC令牌,并且Web应用程序能够击败Api服务器防御,仅接受来自移动应用程序的有效请求.最重要的是,您放置在客户端代码中的任何内容都可以进行反向工程,以便将其复制到另一个客户端中.那么我应该在用例中使用HMAC令牌吗?是的,因为它是洋葱中的一层以上或树中较高的一种水果.我可以做得更好吗?是的,您可以继续阅读...增强和加强安全性再次使用分层防御方法,您应该寻找其他分层方法,这些方法将使您的API服务器对谁和谁正在访问它更有信心因此,如果您的API服务器的客户端只是移动应用,请阅读如何保护移动应用程序的API REST?这个问题的答案.如果您需要保护同时提供移动应用程序和Web应用程序的API,请参见问题未经授权的API调用-保护并仅允许注册的Frontend应用的另一个答案.走得更远现在,我想向您推荐OWASP基金会的出色工作: 《网络安全测试指南》 : 《移动安全测试指南》 :I am developing secure payment APIs, and I want to avoid replay attacks with manipulation of the parameters in the url. For example in the following API call:https://api.payment.com/wallet/transfer?from_account=123&to_account=456&amount=100Once this API call is executed, someone with enough knowledge can execute the same API call by modifying any of the three parameters to his/her own advantage. I have thought of issuing a temporary token (transaction token) for each transaction. But this also doesn't sounds like enough.Can anyone suggest the best way to mitigate replay attacks with parameters tampering? 解决方案 THE API SERVERBefore we dive into addressing your concerns it's important to first clarify a common misconception among developers, that relates to knowing the difference between who vs what is accessing the API server.The difference between who and what is accessing the API server.This is discussed in more detail in this article I wrote, where we can read:If the quoted text is not enough for you to understand the differences, then please go ahead and read the entire section of the article, because without this being well understood you are prone to apply less effective security measures in your API server and clients.SECURITY LAYERS AND PARAMETERS IN THE URLSecurity is all about applying as many layers of defence as possible in order to make the attack as harder and laborious as possible, think of it as the many layers in an onion you need to peel to arrive to the center one.Attackers will always look for the most easy targets, the lower hanging fruit in the tree, because they don't want to resort to use a ladder when they can take the fruit from another tree with lower hanging fruit ;)So one of the first layers of defense is to avoid using parameters in the url for sensitive calls, thus I would use a POST request with all the parameters in the body of the request, because this type of request cannot be done by simply copy paste the url into the browser or any other tool, thus they require more effort and knowledge to be performed, aka the fruit is more high in the tree for the attacker.Another reason is that GET requests end up in the logs of the servers, thus can be accidentally exposed and easily replayed.REPLAY ATTACKS FOR API CALLSYes they can, and they can learn how your API works even if you don't have public documentation for it, they just need to reveres engineer it with the help of any open source tool for mobile apps and web apps.Yes it's not enough because this temporary token can be stolen via a MitM attack, just like a show in the article Steal That Api Key With a Man in the Middle Attack:So after performing the MitM attack to steal the token it's easy to use curl, Postman or any other similar tool to make the requests to the API server just like if you are the genuine who and what the API server expects.MITIGATE REPLAY ATTACKSImproving on Existing Security DefenceThis approach is good but not enough as you alreay noticed, but you can improve it, if not have done it already, by making this temporary token usable only one time.Another important defence measure is to not allow the requests with same amount and same recipients(from_account, to_account) be repeated in sequence, even if they have a new temporary token.Also don't allow requests from the same source to be made to fast, specially if they are intended to come from human interactions.This measures on their own will not totally solve the issue, but add some more layers into the onion.Using HMAC for the One Time TokenIn order to try to help the server to be confident about who and what is making the request you can use a Keyed-Hash Message Authentication Code (HMAC) which is designed to prevent hijacking and tampering, and as per Wikipedia:So you could have the client creating an HMAC token with the request url, user authentication token, your temporary token, and the time stamp that should be also present in a request header. The server would then grab the same data from the request and perform it's own calculation of the HMAC token, and only proceed with the request if it's own result matches the one for the HMAC token header in the request.For a practical example of this in action you can read part 1 and part 2 of this blog series about API protection techniques in the context of a mobile app, that also features a web app impersonating the mobile app.So you can see here how the mobile app calculates the HMAC, and here how the Api server calculates and validates it. But you can also see here how the web app fakes the HMAC token to make the API server think that the requests is indeed from who and what it expects to come from, the mobile app.Mobile App Code::/** * Compute an API request HMAC using the given request URL and authorization request header value. * * @param context the application context * @param url the request URL * @param authHeaderValue the value of the authorization request header * @return the request HMAC */ private fun calculateAPIRequestHMAC(url: URL, authHeaderValue: String): String { val secret = HMAC_SECRET var keySpec: SecretKeySpec // Configure the request HMAC based on the demo stage when (currentDemoStage) { DemoStage.API_KEY_PROTECTION, DemoStage.APPROOV_APP_AUTH_PROTECTION -> { throw IllegalStateException("calculateAPIRequestHMAC() not used in this demo stage") } DemoStage.HMAC_STATIC_SECRET_PROTECTION -> { // Just use the static secret to initialise the key spec for this demo stage keySpec = SecretKeySpec(Base64.decode(secret, Base64.DEFAULT), "HmacSHA256") Log.i(TAG, "CALCULATE STATIC HMAC") } DemoStage.HMAC_DYNAMIC_SECRET_PROTECTION -> { Log.i(TAG, "CALCULATE DYNAMIC HMAC") // Obfuscate the static secret to produce a dynamic secret to initialise the key // spec for this demo stage val obfuscatedSecretData = Base64.decode(secret, Base64.DEFAULT) val shipFastAPIKeyData = loadShipFastAPIKey().toByteArray(Charsets.UTF_8) for (i in 0 until minOf(obfuscatedSecretData.size, shipFastAPIKeyData.size)) { obfuscatedSecretData[i] = (obfuscatedSecretData[i].toInt() xor shipFastAPIKeyData[i].toInt()).toByte() } val obfuscatedSecret = Base64.encode(obfuscatedSecretData, Base64.DEFAULT) keySpec = SecretKeySpec(Base64.decode(obfuscatedSecret, Base64.DEFAULT), "HmacSHA256") } } Log.i(TAG, "protocol: ${url.protocol}") Log.i(TAG, "host: ${url.host}") Log.i(TAG, "path: ${url.path}") Log.i(TAG, "Authentication: $authHeaderValue") // Compute the request HMAC using the HMAC SHA-256 algorithm val hmac = Mac.getInstance("HmacSHA256") hmac.init(keySpec) hmac.update(url.protocol.toByteArray(Charsets.UTF_8)) hmac.update(url.host.toByteArray(Charsets.UTF_8)) hmac.update(url.path.toByteArray(Charsets.UTF_8)) hmac.update(authHeaderValue.toByteArray(Charsets.UTF_8)) return hmac.doFinal().toHex() }API server code:if (DEMO.CURRENT_STAGE == DEMO.STAGES.HMAC_STATIC_SECRET_PROTECTION) { // Just use the static secret during HMAC verification for this demo stage hmac = crypto.createHmac('sha256', base64_decoded_hmac_secret) log.info('---> VALIDATING STATIC HMAC <---') } else if (DEMO.CURRENT_STAGE == DEMO.STAGES.HMAC_DYNAMIC_SECRET_PROTECTION) { log.info('---> VALIDATING DYNAMIC HMAC <---') // Obfuscate the static secret to produce a dynamic secret to use during HMAC // verification for this demo stage let obfuscatedSecretData = base64_decoded_hmac_secret let shipFastAPIKeyData = new Buffer(config.SHIPFAST_API_KEY) for (let i = 0; i < Math.min(obfuscatedSecretData.length, shipFastAPIKeyData.length); i++) { obfuscatedSecretData[i] ^= shipFastAPIKeyData[i] } let obfuscatedSecret = new Buffer(obfuscatedSecretData).toString('base64') hmac = crypto.createHmac('sha256', Buffer.from(obfuscatedSecret, 'base64')) } let requestProtocol if (config.SHIPFAST_SERVER_BEHIND_PROXY) { requestProtocol = req.get(config.SHIPFAST_REQUEST_PROXY_PROTOCOL_HEADER) } else { requestProtocol = req.protocol } log.info("protocol: " + requestProtocol) log.info("host: " + req.hostname) log.info("originalUrl: " + req.originalUrl) log.info("Authorization: " + req.get('Authorization')) // Compute the request HMAC using the HMAC SHA-256 algorithm hmac.update(requestProtocol) hmac.update(req.hostname) hmac.update(req.originalUrl) hmac.update(req.get('Authorization')) let ourShipFastHMAC = hmac.digest('hex') // Check to see if our HMAC matches the one sent in the request header // and send an error response if it doesn't if (ourShipFastHMAC != requestShipFastHMAC) { log.error("\tShipFast HMAC invalid: received " + requestShipFastHMAC + " but should be " + ourShipFastHMAC) res.status(403).send() return } log.success("\nValid HMAC.")Web APP code:function computeHMAC(url, idToken) { if (currentDemoStage == DEMO_STAGE.HMAC_STATIC_SECRET_PROTECTION || currentDemoStage == DEMO_STAGE.HMAC_DYNAMIC_SECRET_PROTECTION) { var hmacSecret if (currentDemoStage == DEMO_STAGE.HMAC_STATIC_SECRET_PROTECTION) { // Just use the static secret in the HMAC for this demo stage hmacSecret = HMAC_SECRET } else if (currentDemoStage == DEMO_STAGE.HMAC_DYNAMIC_SECRET_PROTECTION) { // Obfuscate the static secret to produce a dynamic secret to // use in the HMAC for this demo stage var staticSecret = HMAC_SECRET var dynamicSecret = CryptoJS.enc.Base64.parse(staticSecret) var shipFastAPIKey = CryptoJS.enc.Utf8.parse($("#shipfast-api-key-input").val()) for (var i = 0; i < Math.min(dynamicSecret.words.length, shipFastAPIKey.words.length); i++) { dynamicSecret.words[i] ^= shipFastAPIKey.words[i] } dynamicSecret = CryptoJS.enc.Base64.stringify(dynamicSecret) hmacSecret = dynamicSecret } if (hmacSecret) { var parser = document.createElement('a') parser.href = url var msg = parser.protocol.substring(0, parser.protocol.length - 1) + parser.hostname + parser.pathname + idToken var hmac = CryptoJS.HmacSHA256(msg, CryptoJS.enc.Base64.parse(hmacSecret)).toString(CryptoJS.enc.Hex) return hmac } } return null}As you can see the way the HMAC token is calculated across mobile app, Api server and the Web app are identical in the semantics of the logic, thus resulting in the same HMAC token, and this way the Web app is able to defeat the Api server defense to only accept valid request from the mobile app.The bottom line here is that anything you place in the client code can be reverse engineered in order to replicate it in another client. So should I use HMAC tokens in my use case?Yes, because it's one more layer in the onion or a fruit more high in the tree.Can I do better?Yes you can do, just keep reading...Enhance and Strength the SecurityGoing with the layered defence approach once more, you should look to other layered approaches that will allow your API server to be more confident about who and waht is accessing it.So if the clients of you API server are only mobile apps, then please read this answer for the question How to secure an API REST for mobile app?.In the case you need to secure an API that serves both a mobile and web app, then see this another answer for the question Unauthorized API Calls - Secure and allow only registered Frontend app.GOING THE EXTRA MILENow I would like to recommend you the excellent work of the OWASP foundation:The Web Security Testing Guide:The Mobile Security Testing Guide: 这篇关于如何通过参数操作保护REST API免受重放攻击?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持! 上岸,阿里云!
08-01 18:29