欢迎回到本系列,我们一直在学习如何使用 AI 构建 Web 应用程序。
到目前为止,在本系列中,我们已经创建了一个可用的应用程序,它使用人工智能来确定谁会在两个用户提供的对手之间的战斗中获胜,并生成文本响应和图像。它有效,但我们一直在走幸福的道路。
在这篇文章中,我们将通过考虑错误处理和安全问题来讨论当事情不顺利时会发生什么。
处理错误的 HTTP 请求
要处理的第一个问题是我们的 HTTP 请求。到目前为止,我们只是假设从客户端到服务器的请求能够正常工作。
常量响应 = 等待 jsFormSubmit(form)
// 用响应做一些事情
这是一个错误。我们必须考虑服务器遇到错误或返回错误状态代码的情况。
在现实应用程序中,我们希望有一个复杂的通知服务,以便在发生不同错误(服务器错误、验证错误、授权错误、未找到错误等)时与用户进行通信。最好结合一个错误和错误跟踪软件,这样您就可以收到任何问题的通知。
对于今天的例子,折扣品牌就足够了。我们将检查响应ok< /code> 属性,如果响应不好,我们将只是
提醒
用户出现错误。
常量响应 = 等待 jsFormSubmit(form)
如果(!response.ok){
状态.isLoading = false
alert("请求遇到问题。")
返回
}
上面的代码只考虑了客户端和服务器之间的HTTP请求。不要忘记,我们在服务器和 OpenAI 之间还有另一个请求。
考虑 OpenAI 返回错误状态代码的场景。我们应该如何将其传达给客户端上的最终用户?这对于每个应用程序来说也是棘手且独特的。为了方便起见,我们可以对 response.ok
属性进行类似的检查。如果出现错误请求,您将再次想要报告错误,并可能使用相同的状态代码响应用户。我建议不要将响应消息传递给客户端,以防它包含敏感数据。
常量响应=等待获取('https://api.openai.com/v1/chat/completions', {
// ... 获取选项
})
如果(!response.ok){
报告错误(响应)
抛出错误(response.status,'错误:服务不可用');
}
这种错误处理非常初级,我就这样保留它,因为不幸的是,我从未见过两个应用程序以相同的方式处理错误。这是非常主观的。我只想说,您应该花时间思考您的应用程序在发生错误时应如何表现。您如何在内部报告,如何与用户沟通?
当用户故意尝试破坏某些东西时会发生什么......?
处理错误的用户输入
除了遵循我们假设每个 HTTP 请求始终有效的幸福路径之外,我们还假设每个用户都是仁慈的。这是另一个错误。有时用户是恶意的。很多时候,他们只是很愚蠢。我们应该考虑到两者。
每当您收到用户提交的数据时,您都必须对其进行验证。在我们的应用程序中,我们希望用户提交两个对手。如果他们只提交一个字符串,或者不提交任何字符串,或者提交空字符串,会发生什么情况?在将结构错误的提示发送给 OpenAI 之前,我们可能应该尽早发现这一点。
我们可以添加 HTML Zod。我们可以使用
npm install zod
来安装它。
Zod 允许我们定义一个用于验证输入数据的模式。如果输入与架构不匹配,Zod 可以抛出错误或报告错误。
在我们的应用中,我们通过 requestEvent.parseBody()
方法接收用户输入,该方法将提交的表单数据作为包含 opponent1
和 的对象返回>opponent2
属性。因此,我们需要做的是创建一个验证架构,然后将表单数据传递到其中一种架构验证方法中。
我更喜欢不抛出错误,而是取回带有验证信息的对象。这样,我就可以自己添加逻辑来处理不良数据。
在我的onPost
中间件 ,在做太多工作之前,让我们确保我们拥有正确的数据:
从 'zod' 导入 { z }
// ...
导出 const onPost: RequestHandler = async (requestEvent) => {
const formData = 等待 requestEvent.parseBody()
const 架构 = z.object({
对手1:z.string().min(1),
对手2:z.string().min(1),
})
const 验证 = schema.safeParse(formData)
if (!validation.success) {
requestEvent.json(400, {
错误:validation.error.issues
})
返回
}
// 继续 OpenAI API 请求和响应
}
在上面的代码中,我创建了一个对象架构,该架构应具有两个属性:opponent1
和 opponent2
。这两个属性都是必需的,必须是字符串,并且不能为空。将表单数据传递到架构的 safeParse()
方法将返回一个对象,该对象可以告诉我验证是否成功、错误是什么(如果有)以及验证的数据。
如果数据无效,我会提前从请求处理程序返回 HTTP 400 错误响应解释了错误。 400 是错误请求状态代码。
我喜欢改变的另一件事是表单数据经过验证后的使用方式。 Zod 还在从 safeParse
返回的对象上提供了 data
属性。
const 提示 = 等待提示模板.format({
对手1:validation.data.opponent1,
对手2:validation.data.opponent2
})
在我们的示例中,无论我们直接使用它还是直接使用表单数据,都没有太大区别,但是养成使用 data 属性的习惯是很好的,因为 Zod 会将数据强制为适当的格式。表单数据和查询参数几乎总是以字符串形式接收,但如果您的 Zod 模式需要一个数字,它会尝试为您强制转换它,将字符串 "420"
之类的内容转换为数字 <代码>420。
这涵盖了用户发送丢失数据或数据不足的情况,但是发送过多数据又如何呢?
通过为用户提供无限的输入长度,并将其直接注入到我们的提示中,我们为用户创建大量提示打开了大门,这将需要大量代币并花费我们金钱。为什么我们不为输入添加最大长度以使其更适合此应用程序?
我们可以为服务器端验证架构和客户端验证属性添加最大长度。
通过减少用户可以提供的数据量,我们正在减少 API 请求可能使用的令牌量。
此步骤还限制了用户操作提示的灵活性。考虑这样一个事实:用户可能会提供包含应用程序恶意指令的“对手”。
这很好地解决了人工智能应用程序的一个非常重要的安全问题。
处理注入攻击
我们正在进行一些基本验证,以确保从用户处获得的数据类型正确,但我们不会检查他们发送给我们的内容。我们只是盲目地将其插入我们的提示中并将其发送出去,这使我们面临一种非常有趣的攻击,称为提示注入攻击。
如果您已经完成了使用 SQL 构建应用程序的任何工作,这可能听起来类似于 SQL 注入攻击,因为它确实如此。 SQL注入攻击是指用户提交一些正确类型的数据,但其中包含如果运行可能有害的SQL命令。
这是一个例子。假设我们的应用程序有一些 SQL 逻辑来根据提供的输入按 ID 选择用户:
const query = "SELECT * FROM Users WHERE UserId = " + inputId;
攻击者可以提供字符串'1 OR 1=1'
作为输入,并返回所有用户的信息。这很糟糕,但您可以通过使用参数化查询、存储过程或转义用户输入来避免这种情况。除非您正在编写原始 SQL 查询,否则大多数工具都可以防止注入。如果您感兴趣,请参阅有关 OWASP 预防的更多信息。
提示注入有点不同,因为提示没有包含您可以搜索的特定关键字的结构化语言。您(或用户)提供的任何内容都是有效的提示,并且您所写的内容和用户所写的内容之间没有简单的界限。
值得赞扬的是,OpenAI 确实包含了快速工程策略,鼓励使用分隔符来指示输入的不同部分。这样,您可以帮助 AI 识别不同的身份,例如系统和用户。
它可能看起来像这样:
翻译分隔符“~~~~~~”后的文本:
~~~~~~
[待翻译的文字]
这是一个很大的改进,因为它在系统和用户输入之间提供了更清晰的分离。尽管如此,它也并非没有差距。
Simon Willison 指出了几个即时注入攻击的示例以及为什么它可能永远不会发生已解决的问题:
这有点可怕,应该让您在构建人工智能驱动的应用程序时三思而后行。但这有点像潘多拉魔盒。尽管人工智能存在固有的缺陷,但它仍将继续存在。我的建议是让自己了解最新的攻击媒介并纳入多个安全层。
结论
在构建应用程序时牢记幸福的道路固然很棒,但我们也必须解决不那么幸福的道路。熟悉故障点和漏洞并适当解决它们非常重要。这使我们的应用程序对用户来说更安全、更可靠。
在这篇文章中,我们讨论了:
- 当我们的应用遇到 HTTP 错误时会发生什么?
- 我们如何验证用户输入?
- AI 应用有哪些专门的安全问题?
这不是一个全面的列表,但我希望它可以作为一个好的起点。随着这项工作的完成,我认为我们已经准备好向全世界推出我们的应用程序了。我们将在下一篇文章中做到这一点。
非常感谢您的阅读。如果您喜欢这篇文章,并且想支持我,最好的方法是分享 和在 Twitter 上关注我。