如何通过参数操作保护 REST API 免受重放攻击?

use*_*567 4 security api rest

我正在开发安全支付 API,我希望通过操纵 url 中的参数来避免重放攻击。例如在以下 API 调用中:

https://api. payment.com/wallet/transfer?from_account=123&to_account=456&amount=100

一旦执行此 API 调用,具有足够知识的人就可以通过修改这三个参数中的任何一个来执行相同的 API 调用,以满足他/她自己的利益。我曾想过为每笔交易发行一个临时代币(交易代币)。但这听起来还不够。

谁能建议通过参数篡改来减轻重放攻击的最佳方法?

Exa*_*a37 7

API服务器

我正在开发安全支付 API,我希望通过操纵 url 中的参数来避免重放攻击。

在我们深入解决您的问题之前,首先澄清开发人员中的一个常见误解非常重要,该误解涉及了解访问 API 服务器的人员内容之间的区别。

什么访问 API 服务器之间的区别。

我写的这篇文章对此进行了更详细的讨论,我们可以在其中阅读:

向 API 服务器发出请求的是什么。它真的是您的移动应用程序的真实实例,还是机器人、自动化脚本或攻击者使用 Postman 等工具手动探查您的 API 服务器?

我们可以通过多种方式(例如使用 OpenID Connect 或 OAUTH2 流)对移动应用程序的用户进行身份验证、授权和识别

如果引用的文本不足以让您理解差异,那么请继续阅读本文的整个部分,因为如果没有充分理解这一点,您很容易在 API 服务器和客户端中应用不太有效的安全措施。

URL 中的安全层和参数

例如,在以下 API 调用中: https://api. payment.com/wallet/transfer?from_account=123&to_account=456&amount=100

安全性就是应用尽可能多的防御层,以使攻击尽可能困难和费力,将其视为需要剥掉洋葱中的多层才能到达中间层。

攻击者总是会寻找最容易的目标,即树上垂下的果实,因为当他们可以从另一棵树上摘下果实时,他们不想诉诸使用梯子;)

因此,第一层防御是避免在敏感调用中使用 url 中的参数,因此我将使用请求正文中包含所有参数的 POST 请求,因为这种类型的请求不能通过简单的复制粘贴来完成url 进入浏览器或任何其他工具,因此它们需要更多的努力和知识来执行,也就是说,对于攻击者来说,果实在树上更高。

另一个原因是 GET 请求最终会出现在服务器的日志中,因此可能会被意外暴露并很容易重播。

API 调用的重放攻击

一旦执行此 API 调用,具有足够知识的人就可以通过修改这三个参数中的任何一个来执行相同的 API 调用,以满足他/她自己的利益。

是的,他们可以,即使您没有公共文档,他们也可以了解您的 API 是如何工作的,他们只需要借助任何适用于移动应用程序和 Web 应用程序的开源工具来对其进行工程设计即可。

我曾想过为每笔交易发行一个临时代币(交易代币)。但这听起来还不够。

是的,这还不够,因为这个临时令牌可以通过 MitM 攻击被窃取,就像文章“利用中间人攻击窃取 Api 密钥”中的展示一样:

因此,在本文中,您将了解如何设置和运行 MitM 攻击,以拦截您控制下的移动设备中的 https 流量,以便窃取 API 密钥。最后,您将在较高层面上了解如何缓解 MitM 攻击。

因此,在执行 MitM 攻击以窃取令牌后curl,很容易使用它Postman或任何其他类似的工具向 API 服务器发出请求,就像您是API 服务器所期望的真实身份一样。

减轻重放攻击

改进现有安全防御

我曾想过为每笔交易发行一个临时代币(交易代币)。但这听起来还不够。

这种方法很好,但还不够,正如您已经注意到的那样,但是您可以通过使该临时令牌只能使用一次来改进它(如果还没有这样做的话)。

另一个重要的防御措施是不允许具有相同金额和相同接收者(from_account, to_account)的请求按顺序重复,即使它们具有新的临时令牌。

也不要允许来自同一来源的请求过快,特别是如果它们旨在来自人类交互。

这些措施本身并不能完全解决问题,而是会在洋葱中添加更多层。

使用 HMAC 作为一次性令牌

为了帮助服务器对发出请求的人和内容充满信心,您可以使用密钥哈希消息身份验证代码 (HMAC) ,代码旨在防止劫持和篡改,根据维基百科:

在密码学中,HMAC(有时扩展为密钥哈希消息身份验证代码或基于哈希的消息身份验证代码)是一种特定类型的消息身份验证代码 (MAC),涉及加密哈希函数和秘密加密密钥。与任何 MAC 一样,它可用于同时验证数据完整性和消息的真实性。

因此,您可以让客户端使用请求 url、用户身份验证令牌、临时令牌以及也应出现在请求标头中的时间戳创建 HMAC 令牌。然后,服务器将从请求中获取相同的数据,并执行自己的 HMAC 令牌计算,并且只有在其自己的结果与请求中的 HMAC 令牌标头匹配时才继续处理该请求。

有关实际操作的示例,您可以阅读本博客系列的第 1 部分和第 2部分,了解移动应用程序上下文中的 API 保护技术,其中还包含一个模拟移动应用程序的 Web 应用程序。

因此,您可以在此处了解移动应用程序如何计算 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()
   }
Run Code Online (Sandbox Code Playgroud)

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.")
Run Code Online (Sandbox Code Playgroud)

网页应用程序代码

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
}
Run Code Online (Sandbox Code Playgroud)

注意:虽然上面的代码没有使用与您在案例中使用的完全相同的参数,但它是您了解其基础知识的一个很好的起点。

正如您所看到的,HMAC 令牌在移动应用程序中的计算方式、Api 服务器和 Web 应用程序在逻辑语义上是相同的,从而产生相同的 HMAC 令牌,这样 Web 应用程序就能够击败 Api服务器防御仅接受来自移动应用程序的有效请求。

这里的底线是,您放置在客户端代码中的任何内容都可以进行逆向工程,以便将其复制到另一个客户端中。那么我应该在我的用例中使用 HMAC 令牌吗?是的,因为它是洋葱中的又一层,或者是树上更高的水果。

我可以做得更好吗?是的,你可以做,只需继续阅读......

增强和加强安全性

谁能建议通过参数篡改来减轻重放攻击的最佳方法?

再次采用分层防御方法,您应该寻找其他分层方法,这些方法将使您的 API 服务器对访问它人和内容更加有信心。

因此,如果您的 API 服务器的客户端只是移动应用程序,那么请阅读以下问题的答案:如何保护移动应用程序的 API REST?

如果您需要保护同时为移动和 Web 应用程序提供服务的 API,请参阅问题未经授权的 API 调用 - 安全并仅允许注册的前端应用程序的另一个答案

更加努力

现在我向大家推荐OWASP基金会的优秀作品:

网络安全测试指南

OWASP Web 安全测试指南包括用户可以在自己的组织中实施的“最佳实践”渗透测试框架和描述测试最常见 Web 应用程序和 Web 服务安全问题的技术的“低级”渗透测试指南。

移动安全测试指南

移动安全测试指南 (MSTG) 是移动应用安全开发、测试和逆向工程的综合手册。