Cloudflare API Shield 简介

2022-04-13 00:00:00 模式 客户端 请求 应用程序 证书

API 是连入互联网的现代应用程序的命脉。它们每分每秒都在执行来自移动应用程序的请求:下达这份外卖订单、“点赞”这张图片、发送命令到 IoT 设备、解锁车门、启动洗涤周期,通知有人刚跑完五千米,以及不计其数的其他指令。

意图执行未经授权操作或泄露数据的攻击四处蔓延,也将这些 API 作为攻击目标。正如 Gartner 数据所示,“到 2021 年,90% 支持 Web 的应用程序因为开放 API 而非 UI 而具有更大的攻击表面,2019 年的比例为 40%”,并且“Gartner 预计到 2022 年,API 滥用将从不常见的攻击手段转变为频繁的攻击手段,导致企业 Web 应用程序发生数据泄露”[1][2]。在每秒穿越 Cloudflare 网络的 1800 万个请求中,50% 是针对 API 的,其中大多数请求因为恶意而被阻止。

为了应对这些威胁,Cloudflare 通过使用强大的基于客户端证书的身份识别和严格的基于模式的验证,来简化 API 的安全保护。截至今天,这些功能已在新发产品“API Shield”中面向我们的所有计划免费提供给客户。安全性优势目前也已扩展到基于 gRPC 的 API ,这类 API 使用二进制格式(例如协议缓冲区)而非 JSON,在我们客户群中也越来越受到欢迎。

继续阅读可进一步了解新功能的更多信息;或者,直接跳转至“演示”段落,获取有关如何开始配置条 API Shield 规则的示例。

正向安全模型和客户端证书

所谓“正向安全”模型,指的是仅允许已知行为和身份而拒绝其他一切的模型。它与 Web 应用程序防火墙(WAF)实施的传统“负向安全性”模型相反,后者允许除了源自有问题的 IP、ASN、国家或地区的请求或具有问题签名(SQL 注入行为等)的请求以外的所有内容。

为 API 实施正向安全模型是消除证书填充攻击和其他自动扫描工具的噪声的直接方法。若要采用正向模型,步是部署强大的身份验证,例如双向 TLS 身份验证,这种身份验证不易受到重用或共用密码的影响。

我们在 2014 年通过推出 Universal SSL 来简化服务器证书的颁发,与之类似,API Shield 可将颁发客户端证书的过程缩减为只需点击 Cloudflare 仪表板中的几个按钮。通过提供完全托管的私有公钥基础结构(PKI),您可以专注于开发应用程序和功能,而不必操作和保护自己的证书颁发机构(CA)。

使用模式验证执行有效的请求

一旦开发人员能够确信只有合法客户端(持有 SSL 证书)才可连接他们的 API,那么实施正向安全模型的下一步就是确保这些客户端发出有效的请求。从设备提取客户端证书并在其他地方重用比较困难,但也并非毫无可能,因此确保 API 调用符合预期也很重要。

API 开发人员可能没有预料到含有无关输入的请求,如果应用程序直接处理这些请求,那么可能会导致问题。因此,应尽可能在边缘丢弃这些请求。在 API 模式验证工作时,它会将 API 请求的内容(URL 后面的查询参数和 POST 正文的内容)与包含用来规定预期内容的规则的协定或“模式”进行匹配。如果验证失败,则阻止 API 调用,以保护源站免受请求或恶意有效载荷的侵害。

模式验证当前处于 JSON 有效载荷封闭测试中,gRPC/协议缓冲区则已在路线图中有相应计划。如果您想参加测试,请打开主题为“API Schema Validation Beta”的支持票证。测试结束后,我们计划将模式验证作为 API Shield 用户界面的一部分提供。

演示

为演示如何保护为 IoT 设备和移动应用程序提供支持的 API,我们制作了一个使用客户端证书和模式验证的 API Shield 演示。

由 IoT 设备(演示中为带有外置红外温度传感器的 Raspberry Pi 3 Model B+)采集温度,然后通过 POST 请求传输到受 Cloudflare 保护的 API。随后通过 GET 请求检索温度,再将其显示在用 Swift for iOS 开发的移动应用程序中。

在这两个情形中,API 实际上都是使用 Cloudflare Workers® 和 Workers KV 构建的,但可以被任何可通过互联网访问的端点所代替。

1. API 配置

在将 IoT 设备和移动应用程序配置为与 API 安全通信之前,我们需要引导 API 端点。为了简化示例,同时允许进行其他自定义,我们将 API 实施为 Cloudflare Worker(借用来自 To-Do List 教程的代码)。

在这个特定示例中,通过将源 IP 地址作为密钥将温度存储在 Workers KV 中,但这可以轻松地用客户端证书的值(例如,指纹)来取代。以下代码在发出 POST 后将温度和时间戳存储到 KV 中,并在发出 GET 请求时返回近的 5 个温度。

const defaultData = { temperatures: [] }

const getCache = key => TEMPERATURES.get(key)
const setCache = (key, data) => TEMPERATURES.put(key, data)

async function addTemperature(request) {

    // pull previously recorded temperatures for this client
    const ip = request.headers.get('CF-Connecting-IP')
    const cacheKey = `data-${ip}`
    let data
    const cache = await getCache(cacheKey)
    if (!cache) {
        await setCache(cacheKey, JSON.stringify(defaultData))
        data = defaultData
    } else {
        data = JSON.parse(cache)
    }

    // append the recorded temperatures with the submitted reading (assuming it has both temperature and a timestamp)
    try {
        const body = await request.text()
        const val = JSON.parse(body)

        if (val.temperature && val.time) {
            data.temperatures.push(val)
            await setCache(cacheKey, JSON.stringify(data))
            return new Response("", { status: 201 })
        } else {
            return new Response("Unable to parse temperature and/or timestamp from JSON POST body", { status: 400 })
        }
    } catch (err) {
        return new Response(err, { status: 500 })
    }
}

function compareTimestamps(a,b) {
    return -1 * (Date.parse(a.time) - Date.parse(b.time))
}

// return the 5 most recent temperature measurements
async function getTemperatures(request) {
    const ip = request.headers.get('CF-Connecting-IP')
    const cacheKey = `data-${ip}`

    const cache = await getCache(cacheKey)
    if (!cache) {
        return new Response(JSON.stringify(defaultData), { status: 200, headers: { 'content-type': 'application/json' } })
    } else {
        data = JSON.parse(cache)
        const retval = JSON.stringify(data.temperatures.sort(compareTimestamps).splice(0,5))
        return new Response(retval, { status: 200, headers: { 'content-type': 'application/json' } })
    }
}

async function handleRequest(request) {

    if (request.method === 'POST') {
        return addTemperature(request)
    } else {
        return getTemperatures(request)
    }

}

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

相关文章