Bài viết này được dịch từ bản tiếng Anh. Đọc bản gốc
Kỹ thuật bảo mật cho connector của agent AI: chín nhóm lỗi
Cách StaffOS bảo mật các công cụ bên ngoài mà agent AI gọi: chín nhóm lỗi, từ SSRF và template URL đến phạm vi secret, cùng cơ chế kiểm soát cho từng nhóm.
Một agent AI gọi API bên ngoài có thể đọc dữ liệu riêng tư, đọc nội dung chưa ai kiểm duyệt và có thể gửi dữ liệu ra ngoài. Phần mềm bao quanh mô hình quyết định công cụ được phép truy cập host nào, mỗi request mang theo những gì, ai được phép trỏ một secret đã lưu tới một đích đến và những gì được trả về cho mô hình. Bài viết này trình bày cách StaffOS đưa ra các quyết định đó cho những công cụ bên ngoài mà agent của chúng tôi gọi: chín nhóm lỗi, cơ chế kiểm soát chúng tôi xây dựng cho từng nhóm, và những chi tiết trong phần mềm HTTP phổ biến quyết định một cơ chế kiểm soát có hiệu lực hay không.
Hostname, secret và bản ghi trong các ví dụ đều là hư cấu. Các nhận định về phần mềm bên thứ ba đều trích dẫn tài liệu hoặc mã nguồn của phần mềm đó.
Danh sách kiểm tra
Mười hai mục kiểm tra dưới đây áp dụng cho mọi connector gửi request tới URL do người dùng của nó cấu hình. Mỗi mục nêu một đầu vào dùng để kiểm thử mục đó và phần giải thích mục đó.
- Đánh giá chính URL mà tầng truyền tải sẽ gửi.
http://{x}127.0.0.1/adminphải bị từ chối, không được để HTTP client mở rộng thành địa chỉ loopback (phần 3). - Từ chối mọi địa chỉ không thể truy cập toàn cục, dựa trên một bảng tường minh.
100.100.100.200,64:ff9b::7f00:1và::ffff:127.0.0.1đều phải bị từ chối (phần 2). - Ghim các địa chỉ đã kiểm tra, dùng host đúng như cách viết làm khóa ghim. Request tới
api.example.test.phải kết nối tới địa chỉ đã ghim, không được phân giải lại (phần 2). - Tắt chuyển hướng và các thiết lập proxy lấy từ môi trường. Khi
HTTPS_PROXYđược đặt, request vẫn phải kết nối trực tiếp, và một 302 phải được trả về cho bạn mà không bị đi theo (phần 2). - Đặt cho DNS một thời hạn mà bạn áp đặt được. Một name server không bao giờ trả lời thì không được phép giữ worker lâu hơn thời gian chờ của công cụ (phần 2).
- Ghi mỗi giá trị theo vị trí của nó.
Tan Ah Kowphải đến body JSON dưới dạngTan Ah Kow, dấu ngoặc kép trong một giá trị không được thêm key, và đối số đường dẫn..phải bị từ chối (phần 3). - Giới hạn phạm vi secret đã lưu theo cách sử dụng cũng như theo chủ sở hữu. Thành viên không có quyền admin không được phép thay đổi URL của công cụ gửi secret đã lưu, hoặc chạy công cụ đó (phần 4).
- Dành cho lệnh gọi tự động một đặc tả riêng. Công cụ POST chưa được chủ sở hữu nào xác nhận là chỉ đọc thì không bao giờ được gọi tự động (phần 7).
- Giữ các giá trị của nền tảng ngoài tầm với của mô hình. Lược đồ khai báo đối số
phone_numberbên cạnh{{conversation:phone_number}}phải bị từ chối (phần 8). - Thay thế nội dung lỗi của tầng truyền tải, và che các giá trị đã biết ở mọi cách viết. Lỗi DNS phải trả về một câu cố định không nêu tên host nào, và
{"echo":"sk\u002dlive\u002d4f9c"}phải được trả về ở dạng đã che (phần 5). - Coi những gì không đọc được là không an toàn. Một chế độ xác thực không xác định, một thông tin xác thực mà nền tảng không giải mã được và một body JSON lồng vượt giới hạn của bộ decode đều phải dẫn đến từ chối hoặc bị giữ lại (phần 4 và 5).
- Giới hạn số byte ngay khi chúng đến. Một upstream truyền 100 MB theo dạng stream phải bị cắt tại mức trần, và không có gì vượt quá mức đó được đưa vào bộ đệm (phần 9).
Bốn ví dụ minh họa
Mỗi ví dụ dưới đây vượt qua một guard trông có vẻ đúng. Chúng tôi đã tái hiện cả bốn ví dụ trên máy cục bộ với curl 8.7.1 và PHP 8.5.7.
1. Kiểm tra xem HTTP client của bạn có mở rộng template URL hay không
URL as configured: http://{x}127.0.0.1/admin
Host a parser sees: {x}127.0.0.1 (not an IP address, no DNS answer)
URL Laravel sends: http://127.0.0.1/admin
Một guard coi host không có câu trả lời DNS là vô hại sẽ chấp thuận URL này. Sau đó HTTP client của Laravel đưa URL qua bộ mở rộng RFC 6570 của Guzzle, {x} không có giá trị nên không mở rộng thành gì cả, và request đi tới loopback. Phần 3 giải thích cơ chế mở rộng này và cách để bộ mở rộng không còn gì để làm.
2. Dùng host đúng như cách request viết làm khóa ghim DNS
# The pin applies: cURL connects to 203.0.113.10 without a DNS lookup.
curl --resolve api.example.test:443:203.0.113.10 https://api.example.test/
# The fully qualified name is a different key, so cURL ignores the pin and resolves again.
curl --resolve api.example.test:443:203.0.113.10 https://api.example.test./
Một guard bỏ dấu chấm cuối trước khi dựng khóa ghim đã kiểm tra một câu trả lời DNS nhưng phó mặc kết nối cho một câu trả lời khác. Phần 2 trình bày về việc ghim.
3. Decode trước khi che
{ "echo": "sk\u002dlive\u002d4f9c" }
Tìm kiếm theo byte cho key đã biết sk-live-4f9c không thấy gì trong body này, còn bộ decode JSON trả về chính xác key đó. Phần 5 liệt kê các cách viết khác mà một giá trị đã biết có thể mang.
4. Từ chối giá trị đường dẫn đi ngược lên trên đường dẫn
Template as configured: https://api.example.test/v1/customers/{id}/orders
Argument from the model: id = ..
URL after encoding: https://api.example.test/v1/customers/../orders
Path cURL sends: /v1/orders
rawurlencode('..') trả về .., và libcurl loại bỏ các dot segment trước khi gửi request, như RFC 3986 §5.2.4 mô tả và tài liệu CURLOPT_PATH_AS_IS ghi nhận. Một mô hình bị tin nhắn của khách hàng dẫn dắt có thể đẩy một lệnh gọi lên cấp cao hơn trong API của người vận hành, mang theo chính thông tin xác thực của công cụ. Phần 3 mô tả cách từ chối.
1. Mô hình mối đe dọa cho connector của agent
Connector cho agent AI một cách để hành động bên ngoài nền tảng. Trên StaffOS, một workspace có thể trao cho agent của mình hai loại công cụ bên ngoài: công cụ HTTP API do workspace tự định nghĩa, và endpoint công cụ từ xa được một dịch vụ khác lưu trữ. Cả hai đều chạy trong các cuộc hội thoại thực với khách hàng, nên mô hình mối đe dọa bao gồm khách hàng, mô hình, dịch vụ upstream và tất cả những ai cấu hình công cụ.
Một công cụ HTTP API mô tả một request: một method, một template URL, các header, một chế độ xác thực, một body tùy chọn và một JSON Schema cho các đối số mà mô hình cung cấp. Template có thể chứa giá trị do mô hình cung cấp, chẳng hạn mã đơn hàng, và giá trị do nền tảng điền, chẳng hạn số điện thoại của cuộc hội thoại hiện tại hoặc một secret đã lưu của workspace. Một endpoint công cụ từ xa nêu tên một hàm và lược đồ của hàm đó, và nền tảng chuyển tiếp đối số của mô hình tới URL của endpoint. Endpoint này không dùng Model Context Protocol.
Simon Willison gọi tổ hợp gồm dữ liệu riêng tư, nội dung không đáng tin cậy và giao tiếp ra bên ngoài là bộ ba chết người đối với agent AI. Connector gộp cả ba lại với nhau, và mỗi cơ chế kiểm soát trong bài viết này thu hẹp một chân:
| Chân | Những gì agent nắm giữ | Cơ chế kiểm soát thu hẹp chân này |
|---|---|---|
| Dữ liệu riêng tư | Thông tin khách hàng, và mọi thứ công cụ trả về | Giá trị của nền tảng mà mô hình không thể cung cấp (phần 8); phạm vi workspace và bản ghi (phần 6 và 8); che các giá trị cá nhân mà công cụ đã gửi (phần 5) |
| Nội dung không đáng tin cậy | Tin nhắn của khách hàng và response từ upstream | Kết quả được gắn nhãn là dữ liệu và bị giới hạn kích thước (phần 7 và 9); bản ghi được render nguyên vẹn (phần 10) |
| Giao tiếp ra bên ngoài | Công cụ gửi request, một số mang theo thông tin xác thực | Kiểm soát đích đến (phần 2); dựng request (phần 3); secret được giới hạn phạm vi theo cách sử dụng (phần 4); tự động hóa do chủ sở hữu kiểm soát (phần 6 và 7) |
Các tác nhân
| Tác nhân | Được tin cậy để | Không được tin cậy để |
|---|---|---|
| Khách hàng | Trò chuyện với agent | Chọn nơi công cụ gửi dữ liệu, hoặc xem bản ghi của khách hàng khác |
| Mô hình | Chọn lệnh gọi công cụ từ danh mục được cung cấp | Cung cấp giá trị do nền tảng điền, hoặc thay thế quyết định của chủ sở hữu |
| API upstream hoặc endpoint từ xa | Trả về dữ liệu | Giữ response nhỏ gọn và kịp thời, không lặp lại thông tin xác thực trong nội dung trả về, hoặc ra chỉ dẫn cho mô hình |
| DNS của host do workspace chọn | Không gì cả | Trả lời nhất quán hoặc kịp thời |
| Thành viên workspace không có quyền admin | Chỉnh sửa công cụ thông thường | Quyết định nơi secret đã lưu được gửi đến, hoặc bật các lệnh gọi mà không ai chọn |
| Chủ sở hữu hoặc admin của workspace | Chọn secret, đích đến và tự động hóa | Truy cập địa chỉ loopback, riêng hoặc link-local, hoặc đọc thông tin xác thực của nền tảng |
| Workspace khác | Không gì cả | Biết bất cứ điều gì về workspace này |
Tài sản
- Thông tin xác thực của nền tảng, chẳng hạn các key mà nền tảng dùng với các đối tác nhắn tin.
- Secret của workspace mà công cụ gửi tới API upstream.
- Dữ liệu cá nhân của khách hàng: số điện thoại, username, địa chỉ email, mã định danh bên ngoài và các trường tùy chỉnh được đánh dấu là dữ liệu cá nhân.
- Mạng nội bộ: loopback, các dải riêng RFC 1918, địa chỉ link-local và dịch vụ metadata của cloud.
- Năng lực xử lý: thời gian và bộ nhớ của worker, cửa sổ ngữ cảnh của mô hình và giới hạn số công cụ mỗi request của nhà cung cấp.
- Độ chính xác của những gì agent nói với khách hàng.
- Quyết định của chủ sở hữu: khi chủ sở hữu tắt một công cụ hoặc chọn nơi một secret đã lưu được gửi đến, quyết định đó phải được giữ nguyên.
Chín nhóm lỗi
| # | Nhóm lỗi | Ranh giới | Phần |
|---|---|---|---|
| 1 | Kiểm soát đích đến chiều đi | Từ request đã render đến mạng | 2 |
| 2 | Dựng request | Từ template của workspace đến request đã render | 3 |
| 3 | Phạm vi secret | Từ cấu hình đến request | 4 |
| 4 | Cô lập lỗi và response | Từ response và exception đến mô hình và log | 5 |
| 5 | Phân quyền theo tác động | Từ request thiết lập đến công cụ đã lưu | 6 |
| 6 | Lệnh gọi tự động | Từ tự động hóa đến lúc gửi | 7 |
| 7 | Giá trị của mô hình, workspace và nền tảng | Từ đối số của mô hình đến request | 8 |
| 8 | Giới hạn tài nguyên | Từ mạng đến worker; từ danh mục đến nhà cung cấp | 9 |
| 9 | Tính toàn vẹn của đầu ra | Từ response đến câu trả lời của mô hình | 10 |
2. Kiểm soát đích đến chiều đi
Trong connector, server-side request forgery (SSRF) là khi một URL do workspace cấu hình truy cập tới một địa chỉ mà nó không được phép truy cập: loopback, mạng riêng hoặc dịch vụ metadata của cloud tại 169.254.169.254. Phép kiểm tra URL chỉ ngăn được SSRF khi nó đánh giá đúng host, địa chỉ, proxy và chính sách chuyển hướng mà tầng truyền tải HTTP sẽ dùng, vì vậy phép kiểm tra của chúng tôi kiểm tra chính các đầu vào của tầng truyền tải.
Những cách viết host mà parser nghiêm ngặt không nhận ra
FILTER_VALIDATE_IP của PHP không chấp nhận 2130706433, 0x7f000001, 0177.0.0.1 và 127.1, nên một guard dựa trên nó coi chúng là hostname. DNS không trả về gì cho các giá trị này, còn libcurl kết nối tới cả bốn dưới dạng 127.0.0.1. Vì vậy, một guard cho phép tên có câu trả lời DNS rỗng sẽ fail open. Các cách viết percent-encode và có zone identifier cũng hoạt động tương tự: parse_url() trả về nguyên dạng %31%32%37.0.0.1 và [::1%25lo0], còn cURL chuẩn hóa cả hai thành địa chỉ loopback.
Phép kiểm tra đích đến của chúng tôi chỉ chấp nhận host khi đó là hostname DNS thuần hoặc địa chỉ IP ở dạng chuẩn, và tại thời điểm gửi sẽ từ chối mọi hostname không phân giải được. Phép kiểm tra này chạy trên URL sau khi mọi giá trị, kể cả giá trị của mô hình, đã được ghi vào. Phép kiểm tra địa chỉ từ chối trọn khối mọi dải mà các sổ đăng ký địa chỉ dành cho mục đích đặc biệt IPv4 và IPv6 của IANA đánh dấu là không thể truy cập toàn cục, cùng với multicast. Phạm vi đó bao gồm loopback, 0.0.0.0/8, các dải riêng RFC 1918, không gian địa chỉ dùng chung 100.64.0.0/10 mà carrier-grade NAT và mạng overlay sử dụng, địa chỉ link-local bao gồm địa chỉ metadata của cloud, và các dải dành cho tài liệu và đo kiểm hiệu năng. IPv6 bị giới hạn trong global unicast, 2000::/3, trừ các khối tài liệu, 6to4 và Teredo nằm bên trong nó. Địa chỉ IPv4-mapped, IPv4-compatible, 6to4 và Teredo bị từ chối thẳng, còn địa chỉ thuộc tiền tố NAT64 well-known 64:ff9b::/96 được đánh giá theo địa chỉ IPv4 mà nó mang.
Chính sách này là một bảng dải địa chỉ tường minh, vì các cờ dải địa chỉ của PHP cho qua những địa chỉ không phải địa chỉ công cộng. FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE chấp nhận 100.64.0.1, 198.18.0.1, 192.0.2.1, 224.0.0.1, dạng NAT64 của loopback 64:ff9b::7f00:1 và dạng 6to4 2002:7f00:1::1 trên mỗi phiên bản PHP chúng tôi đã kiểm thử (8.1, 8.2 và 8.5), và trên 8.1 và 8.2 còn chấp nhận cả ::ffff:127.0.0.1. Không gian địa chỉ dùng chung mà nó chấp nhận có chứa một dịch vụ metadata của cloud: dịch vụ của Alibaba Cloud trả lời tại 100.100.100.200. Cùng một bảng địa chỉ dành cho mục đích đặc biệt làm cơ sở cho unit test của chính sách, các phép kiểm tra lúc lưu và các phép kiểm tra lúc gửi.
Địa chỉ đã kiểm tra và địa chỉ được kết nối
Một guard phân giải tên rồi để HTTP client phân giải lại lần nữa đã chấp thuận một câu trả lời DNS nhưng kết nối tới một câu trả lời khác. Một zone DNS rebinding có thể trả về địa chỉ công cộng cho lần kiểm tra và địa chỉ riêng cho lần kết nối, tức một khoảng hở giữa thời điểm kiểm tra và thời điểm sử dụng (CWE-367). Chúng tôi ghim địa chỉ đã được chấp thuận vào kết nối bằng CURLOPT_RESOLVE của cURL, để socket mở tới đúng địa chỉ mà guard đã kiểm tra.
Hai chi tiết quyết định việc ghim có hiệu lực hay không:
- Khóa ghim phải khớp chính xác với host của request. cURL so khớp các mục
CURLOPT_RESOLVEtheo chuỗi host, nên bản ghim choapi.example.testkhông áp dụng cho request tới dạng tên miền đầy đủapi.example.test.có dấu chấm cuối. - Proxy là một bộ phân giải thứ hai. Khi đi qua proxy, cURL gửi
CONNECT host:portvà proxy phân giải tên, nên bản ghim không còn quyết định đích đến.
Đích chuyển hướng là một URL thứ hai mà guard chưa từng thấy. Cả bốn HTTP client dưới đây mặc định đi theo chuyển hướng, và ba trong số đó đọc thiết lập proxy từ môi trường:
| HTTP client | Mặc định lấy proxy từ môi trường | Mặc định đi theo chuyển hướng |
|---|---|---|
| PHP, Guzzle 7 | Có: HTTPS_PROXY và NO_PROXY, và HTTP_PROXY trong các tiến trình dòng lệnh |
Có, tối đa 5 |
| Python, requests | Có: http_proxy, https_proxy, no_proxy, all_proxy và dạng viết hoa của chúng |
Có, với mọi method trừ HEAD |
Go, mặc định của net/http |
Có: HTTP_PROXY, HTTPS_PROXY và NO_PROXY |
Có, tối đa 10 |
Node.js, fetch |
Chỉ khi có NODE_USE_ENV_PROXY=1, có từ Node 24.0 và 22.21 |
Có |
Vì vậy, request của connector chạy với chuyển hướng bị tắt, thiết lập proxy từ môi trường bị bỏ qua và địa chỉ đã kiểm tra được ghim. Trong Guzzle, đó là ba tùy chọn request:
// $host is the request's host exactly as written, trailing dot included.
$options = [
'allow_redirects' => false,
'proxy' => '', // an empty proxy overrides HTTP_PROXY and HTTPS_PROXY
'curl' => [CURLOPT_RESOLVE => ["{$host}:{$port}:{$checkedAddress}"]],
];
Tài liệu CURLOPT_PROXY ghi rõ rằng chuỗi proxy rỗng sẽ vô hiệu hóa proxy ngay cả khi một biến môi trường đã đặt proxy. OWASP SSRF Prevention Cheat Sheet khuyến nghị cùng cách tiếp cận: kiểm tra tính hợp lệ của đích đến, phân giải và kiểm tra các địa chỉ của đích đến, và tắt chuyển hướng.
Host mà guard đọc là host mà cURL kết nối tới
Một guard trích xuất host bằng một parser trong khi tầng truyền tải dùng parser khác có thể chấp thuận một host nhưng lại kết nối tới host khác. Các payload kinh điển giấu host thật sau \@, #@, ?@, một dấu @ thứ hai hoặc một dấu gạch chéo đã encode trong userinfo, như trong https://allowed.example.test#@127.0.0.1/. Khi chạy qua cả guard của chúng tôi lẫn tầng truyền tải, mỗi payload này hoặc bị từ chối, hoặc kết nối tới đúng host mà guard đã kiểm tra. Mọi địa chỉ trong câu trả lời DNS đều được kiểm tra theo dải, và kết nối được ghim vào tập địa chỉ đã kiểm tra đó.
DNS cần có thời hạn
dns_get_record() của PHP không nhận thời gian chờ, nên một máy chủ authoritative chậm của tên miền do workspace chọn có thể giữ worker lâu bao nhiêu tùy ý. Đo thời gian tra cứu sau khi nó trả về sẽ đo được độ trễ nhưng không giới hạn được độ trễ đó. Connector của chúng tôi phân giải tên trong một tiến trình riêng, tiến trình này bị dừng tại một thời hạn nằm trong ngân sách thời gian của công cụ, và lần tra cứu không thể hoàn tất bị từ chối giống như một lần tra cứu thất bại.
Parser tài liệu là một client mạng
PhpWord mặc định tải hình ảnh, và với hình VML có relationship được đánh dấu là external, nó tải hình ảnh từ URL mà tài liệu chỉ định. Vì vậy, một file Word được tải lên có thể khiến máy chủ truy xuất bất kỳ địa chỉ nào mà file đó chọn. Trình đọc tài liệu của chúng tôi trích xuất văn bản, việc không bao giờ cần đến hình ảnh, nên nó không tải hình ảnh nào.
Cả hai loại công cụ dùng cùng một guard
Công cụ HTTP API và proxy endpoint từ xa áp dụng cùng một phép kiểm tra đích đến và cùng các tùy chọn của tầng truyền tải. Proxy này cũng chỉ chấp nhận URL http và https tuyệt đối có nêu host. Bản ghi audit của một lượt chạy thử thủ công lưu mã định danh của công cụ thay vì URL của nó.
3. Dựng request
Một request đã qua phép kiểm tra vẫn có thể thay đổi trước khi được gửi. Một số HTTP client coi mọi URL là template, header và body JSON đều có ngữ pháp riêng, và các parser query string đọc tên tham số theo những cách khác nhau. Chúng tôi ghi mọi giá trị theo vị trí nó chiếm, trong một lần duyệt duy nhất, và từ chối mọi thứ mà một giai đoạn sau sẽ diễn giải lại.
HTTP client mở rộng template URL
HTTP client của Laravel hỗ trợ template URI, và method send() của nó đưa mọi URL qua bộ mở rộng RFC 6570 của Guzzle, bất kể bên gọi có gắn tham số template nào hay không. Theo RFC 6570 §3.2.1, một biểu thức không có giá trị sẽ không mở rộng thành gì cả:
Template as configured: https://api.example.test/members/{member_id}
Sent when unfilled: https://api.example.test/members/
URL thứ hai truy vấn toàn bộ collection. Các biểu thức có toán tử như {?member_id}, {/member_id} và {+member_id} cũng không mở rộng thành gì cả theo cách tương tự. Mở rộng reserved ({+var}) và mở rộng fragment ({#var}) để /, ?, # và @ đi qua mà không encode, nên một đoạn nằm trong dấu ngoặc nhọn có thể thay đổi đường dẫn, query hoặc host của URL. Một biểu thức không có giá trị cũng có thể làm dịch chuyển host, như ví dụ minh họa đầu tiên cho thấy. Một phép kiểm tra chỉ nhận ra {name} sẽ cho tất cả những trường hợp này đi qua.
Bộ mở rộng của Guzzle trả về nguyên vẹn một chuỗi không chứa dấu ngoặc nhọn nào. Renderer của chúng tôi dựa vào điều đó. Nó tự ghi mọi giá trị, nối các đối số không được placeholder nào dùng vào query của một GET hoặc DELETE, và dừng lệnh gọi trước khi bất cứ thứ gì được gửi nếu còn sót bất kỳ { hoặc } nào. Thông báo lỗi cho biết một đối số đã khai báo bị thiếu, một đối số chưa khai báo đã được dùng, hay chính template không hợp lệ.
Một renderer, một lần duyệt
Một renderer duy nhất ghi mọi template mà công cụ gửi (URL, header, body và thông tin xác thực) cho mọi luồng gửi template đó: lệnh gọi của chính mô hình, pre-fetch tự động được mô tả ở phần 7 và các lượt chạy thử thủ công. Renderer ghi đối số của mô hình và giá trị của nền tảng trong cùng một lần duyệt, và giá trị đã ghi không bao giờ được đọc lại. Vì vậy, một giá trị đã ghi không thể hoàn thiện một placeholder, và một đối số không thể biến {{secret:{key}}} thành tham chiếu tới một secret do mô hình chọn.
Giá trị được ghi theo vị trí của nó
| Vị trí của giá trị | Cách ghi | Bị từ chối |
|---|---|---|
| URL | Percent-encode, kể cả secret đã lưu | Placeholder chưa khai báo, ngay cả khi lệnh gọi có truyền đối số đó; giá trị bị thiếu hoặc rỗng; giá trị tạo thành segment đường dẫn . hoặc .. |
| Header | Nguyên văn | Đối số chứa ký tự xuống dòng, ký tự điều khiển khác tab, hoặc văn bản không phải ASCII; placeholder chưa khai báo; giá trị bị thiếu hoặc rỗng |
| Body, bên trong chuỗi JSON | Dưới dạng nội dung chuỗi JSON, chỉ escape ở những chỗ JSON yêu cầu | Đối số đã khai báo nhưng lệnh gọi bỏ qua |
| Body, tại vị trí giá trị | Dưới dạng giá trị JSON mà mô hình đã gửi: number, boolean, null, array hoặc object | Đối số đã khai báo nhưng lệnh gọi bỏ qua |
Percent-encoding giữ nguyên ., nên một giá trị vẫn có thể tạo thành dot segment trong đường dẫn URL, thứ mà libcurl loại bỏ trước khi gửi (ví dụ minh họa thứ tư). Renderer theo dõi những ký tự nào của đường dẫn đến từ một giá trị và từ chối segment . hoặc .. do một giá trị tạo thành, bất kể giá trị đó đến từ mô hình, nền tảng hay một secret đã lưu. Dot segment do người vận hành viết vào template được gửi nguyên văn: đó là đường dẫn của chính người vận hành trên chính host của người vận hành.
Percent-encode một giá trị trong body khiến upstream đọc nguyên văn phần văn bản đã encode: Tan Ah Kow đến nơi dưới dạng Tan%20Ah%20Kow, và việc tra cứu theo tên đó không tìm thấy gì. Ghi văn bản thô vào body cho phép một dấu ngoặc kép thêm key. Giá trị của nền tảng trong body phải nằm bên trong một chuỗi JSON, nơi dấu ngoặc kép trong tên của liên hệ được escape; các giá trị tùy chọn mô tả bên dưới là ngoại lệ duy nhất. Body không phải JSON hợp lệ sau khi mọi giá trị đã được ghi vào, hoặc đặt một giá trị vào key của object, sẽ bị từ chối khi công cụ được lưu và một lần nữa khi công cụ chạy. Một đối số mà JSON không thể biểu diễn, chẳng hạn 1e999 của mô hình, vốn decode thành vô cực, sẽ khiến lệnh gọi bị từ chối trên mọi bề mặt. Trong body, dấu ngoặc nhọn mà lược đồ không khai báo được gửi nguyên văn, vì selection set của GraphQL được viết trong dấu ngoặc nhọn.
Những tên tham số mang cùng ý nghĩa
Đối số của mô hình không được placeholder nào dùng sẽ được nối vào query, nên renderer phải ngăn một đối số như vậy trùng với một tham số mà người vận hành đã cố định trong template, chẳng hạn bộ lọc tenant. parse_str() của PHP không thể thực hiện phép so sánh đó: nó chuyển dấu chấm và dấu cách trong tên thành dấu gạch dưới và lồng các tên có dấu ngoặc vuông, nên tenant.id được đọc thành tenant_id và filter[tenant_id] được đọc thành filter. Chúng tôi so sánh tên theo một định danh duy nhất gộp mọi cách đọc: cắt tại dấu [ đầu tiên, quy . và dấu cách về _, và không phân biệt hoa thường. Đối số của mô hình có định danh trùng với một tham số đã có trong URL đã render sẽ bị loại bỏ, và giá trị của người vận hành được giữ nguyên.
Lúc lưu và lúc chạy thống nhất với nhau
Mọi placeholder trong URL hoặc các header được gửi của công cụ phải được khai báo trong lược đồ đối số và đánh dấu là bắt buộc thì công cụ mới được lưu, và mọi placeholder đã khai báo trong body phải là bắt buộc. Lúc chạy, lệnh gọi thiếu một trong số đó bị từ chối trước khi bất cứ thứ gì được gửi. Nếu không, một placeholder tùy chọn sẽ biến mất khỏi request, và khi đó request sẽ trỏ tới một tài nguyên khác với tài nguyên mà người vận hành đã mô tả.
Các header mà request mang theo
Việc một lệnh gọi gửi những header nào được quyết định một lần, từ cấu hình của công cụ, trước khi bất kỳ giá trị nào được phân giải: các header riêng của công cụ, Authorization của chế độ xác thực, một Content-Type khi method gửi body, một Accept, và các header riêng của tầng truyền tải, chẳng hạn Host. Các phép kiểm tra quyết định một lệnh gọi có được gửi đi hay không (giá trị bị thiếu, hàng rào secret ở phần 4, đặc tả pre-fetch ở phần 7) đọc cùng quyết định đó. Một bài kiểm thử sẽ thất bại nếu HTTP client dùng chung của ứng dụng có thêm bất kỳ tùy chọn mặc định hoặc middleware nào, vì công cụ của tenant sẽ mang tùy chọn hoặc middleware đó tới host do tenant chọn.
Giá trị tùy chọn của nền tảng
Một số lệnh tra cứu chấp nhận một trong nhiều mã định danh, chẳng hạn số điện thoại hoặc username. Giá trị của nền tảng được đánh dấu là tùy chọn chỉ có thể nằm ở vị trí giá trị JSON trong body. Giá trị đó được ghi thành chuỗi JSON khi có mặt và thành null khi bị thiếu, và bị từ chối trong URL, header, thông tin xác thực, bên trong dấu ngoặc kép và khi làm key của object. Secret không bao giờ là tùy chọn. Lệnh gọi bị từ chối khi mọi giá trị tùy chọn đều thiếu và không có giá trị nào khác từ hội thoại, liên hệ hoặc trường tùy chỉnh đi vào request, nhờ vậy một lệnh tra cứu không bao giờ được gửi đi mà không có thông tin nào định danh khách hàng.
4. Phạm vi secret
Một secret đã lưu chỉ an toàn khi hai câu hỏi có cùng một câu trả lời: ai được phép quản lý nó, và ai được phép quyết định nó được gửi đến đâu. StaffOS giữ secret đã lưu của mỗi workspace trong kho mã hóa riêng của workspace đó, chỉ phân giải tham chiếu secret từ chính workspace của công cụ, và chỉ cho phép những người được quyền quản lý secret đó thực hiện mọi thay đổi có thể đổi đích đến của một secret đã lưu.
Một kho có phạm vi riêng
Tham chiếu secret trong một công cụ được phân giải từ kho của chính workspace sở hữu công cụ và không từ nơi nào khác. Tham chiếu tới một secret mà workspace chưa lưu sẽ dừng lệnh gọi, và không có gì được gửi đi.
Quản lý secret và sử dụng secret là hai quyền riêng biệt
Giới hạn người được tạo secret vẫn để ngỏ một con đường thứ hai. Một thành viên không đọc được secret đã lưu vẫn có thể chỉnh sửa công cụ gửi secret đó, trỏ công cụ tới một host do họ kiểm soát và chạy thử. Đó chính là confused deputy của Norm Hardy: nền tảng nắm quyền hạn, còn một bên lẽ ra không được sử dụng quyền hạn đó lại chọn cách nó được dùng.
Với công cụ gửi secret đã lưu, chỉ chủ sở hữu hoặc admin của workspace mới có thể:
- thay đổi method, URL, header, xác thực hoặc body của công cụ;
- chạy công cụ thủ công từ trang thiết lập;
- bật lại công cụ sau khi công cụ đã bị tắt.
Chúng tôi chỉ giới hạn quyền với những gì một vai trò thấp hơn không thể tái tạo bằng cách khác. Lượt chạy thử thủ công của công cụ không gửi secret đã lưu nào được mở cho mọi vai trò, vì thành viên vốn đã có thể chỉnh sửa hoặc sao chép công cụ như vậy, nên giới hạn quyền chạy thử cũng không bảo vệ được gì.
Vì sao một lần duyệt duy nhất quan trọng với secret
Một template request có thể trộn lẫn secret đã lưu, giá trị của hội thoại, giá trị của liên hệ và đối số của mô hình. Phân giải chúng trong các lần duyệt riêng biệt cho phép một giá trị chèn thêm một giá trị khác. Nếu secret được thay thế trước rồi kết quả mới được quét tìm placeholder hội thoại, một secret có nội dung tình cờ chứa placeholder hội thoại sẽ kéo số điện thoại của khách hàng vào request. Đảo ngược thứ tự sẽ chuyển cùng lỗi đó sang các giá trị hội thoại chứa văn bản có dạng secret. Lần duyệt duy nhất ở phần 3 chỉ đọc văn bản nguồn của người vận hành, nên không có giá trị được thay thế nào bị phân tích thành placeholder.
Từ chối những gì không phân giải được
Một tham chiếu không khớp với mẫu nào, chẳng hạn placeholder viết sai hoặc không được đóng, không được đi theo dưới dạng văn bản nguyên văn trong header, body hoặc thông tin xác thực. Một mẫu ngoài lỏng tìm mọi đoạn ứng viên, một mẫu trong chặt kiểm tra tính hợp lệ của đoạn đó, và bất kỳ đoạn nào còn chưa được phân giải sẽ khiến lệnh gọi bị từ chối.
Thông tin xác thực cũng fail closed. Một secret phân giải ra không có gì dùng được, một secret chưa lưu trong bearer token hoặc một chế độ xác thực không xác định sẽ dừng lệnh gọi trước khi nó được gửi. Lựa chọn còn lại là một request không được xác thực, và một upstream chấp nhận thao tác ghi ẩn danh sẽ thực thi side effect đó.
Thông tin xác thực mà nền tảng không giải mã được
Thông tin xác thực được mã hóa bằng một khóa mã hóa mà nền tảng không còn giữ thì không thể đọc được, và không gì có thể kiểm tra nó sẽ gửi đi những gì. Công cụ có thông tin xác thực không giải mã được sẽ bị loại khỏi danh mục của agent, và secret đã lưu không giải mã được sẽ phân giải ra không có gì, khiến lệnh gọi bị từ chối. Đối với hàng rào chủ sở hữu hoặc admin, công cụ như vậy được tính là công cụ gửi secret đã lưu. Một lần lưu thông thường giữ nguyên ciphertext đã lưu, nên khóa mã hóa được khôi phục vẫn đọc được nó; chỉ một giá trị do ai đó nhập vào, hoặc một lựa chọn xóa tường minh, mới thay thế nó. Ở những nơi thao tác đọc hiển thị placeholder thay cho thông tin xác thực đã lưu, thao tác ghi từ chối placeholder đó khi nó được dùng làm giá trị, nên một chu trình read-modify-write không thể thay thông tin xác thực thật bằng mặt nạ của nó.
Thông tin xác thực được lưu cho một mục đích
Thông tin xác thực được giữ cho một mục đích không được phép chọn để dùng cho mục đích khác. Proxy endpoint từ xa có thể xác thực bằng thông tin xác thực mà workspace đã lưu, và proxy từ chối một thiết lập xác thực nêu tên một trong các nhà cung cấp mô hình AI của workspace trước khi đọc bất kỳ thông tin xác thực nào. Không cấu hình công cụ nào có thể gửi một key mô hình đã lưu tới host do người cấu hình chọn.
Secret trong log
Một số luồng trao đổi code OAuth đặt client secret trong query string. Meta mô tả luồng trao đổi token của WhatsApp Embedded Signup là một GET tới /oauth/access_token với client_secret nằm trong các tham số query, và HTTP client thường đưa toàn bộ URL vào lỗi truyền tải. Chuỗi biểu diễn exception của PHP còn bao gồm message của mọi exception trước đó trong chain, và chuỗi đó chính là thứ mà log lỗi và bản ghi job thất bại lưu lại. Các lệnh gọi Meta API của chúng tôi có mang thông tin xác thực trong query string sẽ ném lại lỗi truyền tải với mọi query string đã bị xóa và không đính kèm exception gốc.
5. Cô lập lỗi và response
Thông báo lỗi và body của response là hai con đường để thông tin xác thực rời khỏi một lệnh gọi công cụ. Với lỗi truyền tải, chúng tôi trả về một phân loại cùng một câu cố định và bỏ đi văn bản của thư viện. Với response của cả hai loại công cụ, chúng tôi che các thông tin xác thực mà công cụ đã gửi ở mọi cách viết JSON cho phép, và công cụ HTTP API còn che cả các giá trị cá nhân mà nó đã gửi.
Thay thế nội dung lỗi thay vì làm sạch nó
Thư viện HTTP trích dẫn các đầu vào đã gây ra lỗi. libcurl lặp lại hostname trong các thông điệp như Could not resolve host, nên thông tin xác thực đặt trong hostname vẫn tồn tại sau bất kỳ thao tác làm sạch URL nào. Phần kiểm tra hợp lệ header của Guzzle ném ra exception có trích dẫn giá trị header không hợp lệ. Bộ làm sạch phải lường trước mọi vị trí mà thông tin xác thực có thể nằm (tham số query với bất kỳ tên và kiểu viết hoa thường nào, key lặp lại và key có dấu ngoặc vuông, userinfo, segment đường dẫn, hostname, số) và mọi thư viện có thể trích dẫn nó.
Chúng tôi không làm sạch. Request thất bại ở tầng truyền tải trả về một phân loại thuộc một tập nhỏ cùng một câu cố định không chứa gì bắt nguồn từ request hay exception. Đoạn mã duy nhất đọc exception quyết định hai điều: lỗi có phải do hết thời gian chờ hay không, dựa vào mã lỗi của cURL khi có, và response có quá lớn hay không.
Thực thi việc cô lập bằng kiểm tra tĩnh
Một bài kiểm thử kiến trúc phân tích cú pháp mọi công cụ gửi request HTTP và yêu cầu mỗi lệnh gọi đi ra mạng phải nằm trong thân của một khối try có các handler bắt và cô lập lỗi kết nối. Bài kiểm thử làm việc trên cây cú pháp, phân giải các tên được import và tên alias, lần theo các HTTP client được truyền vào qua dependency injection, và coi mọi dạng mã mà nó không đọc được là vi phạm. Một công cụ mới có request không được bảo vệ sẽ làm bộ kiểm thử thất bại.
Handler phải bắt đúng lớp. Trong Laravel, ConnectionException và RequestException là hai lớp anh em cùng nằm dưới HttpClientException, nên handler cho RequestException không bao giờ thấy lỗi DNS hay lỗi hết thời gian chờ.
Che một giá trị đã biết ở mọi cách viết
Khi response phải đến được mô hình, câu hỏi chuyển từ "điều gì có thể là thông tin xác thực?" sang "thông tin xác thực đã biết này xuất hiện ở đâu?". Công cụ HTTP API cung cấp các giá trị mà nó coi là thông tin xác thực: secret đã lưu, thông tin xác thực trong thiết lập xác thực của công cụ, và giá trị của các header có tên cho thấy đó là thông tin xác thực, chẳng hạn Authorization. Công cụ bổ sung các giá trị cá nhân của khách hàng mà nó đã gửi, được mô tả bên dưới, ở từng cách viết mà nó đã dùng khi gửi, kể cả cách viết JSON trong body. Proxy endpoint từ xa cung cấp tập giá trị riêng: thông tin xác thực đã lưu của nó, giá trị của các header có tên là thông tin xác thực, header Cookie và các giá trị cookie dài bên trong, và các phần trong URL của nó có thể mang key, mỗi phần ở dạng nguyên văn và ở dạng đã decode. Cả hai công cụ chuyển tập giá trị của mình cho cùng một bộ che, bộ này tìm từng giá trị ở mọi cách viết mà response có thể dùng.
| Cách viết | Vì sao so khớp byte bỏ sót | Cách xử lý |
|---|---|---|
| Escape trong JSON | RFC 8259 §7 cho phép escape bất kỳ ký tự nào, nên sk\u002dlive không chứa sk-live |
Decode body, che các giá trị đã decode, rồi encode lại |
| Số nguyên lớn | PHP decode số nguyên lớn hơn PHP_INT_MAX thành float trừ khi đặt JSON_BIGINT_AS_STRING, làm mất các chữ số mà phép so khớp so sánh |
Decode số nguyên lớn thành chuỗi |
| Float | Khi ép kiểu float sang chuỗi, PHP mặc định in 14 chữ số có nghĩa, nên 1234567890123456 trở thành 1.2345678901235E+15 |
So sánh số theo giá trị chuẩn |
| Key của object | Upstream có thể trả lại thông tin xác thực dưới dạng key | Kiểm tra cả key lẫn giá trị |
| Xác thực Basic | Header mang base64 của user:password, và upstream đã parse header đó trả về mật khẩu dạng thuần |
Coi cả token đã encode lẫn mật khẩu đã decode là giá trị đã biết |
| Giá trị chồng lấn | str_replace() với mảng có thể viết lại văn bản mà một lần thay thế trước đó đã chèn vào, làm hỏng chuỗi đánh dấu |
Dùng strtr(), hàm này thử key dài nhất trước và không bao giờ quét lại văn bản đã thay thế |
| Lồng sâu | Body JSON lồng vượt giới hạn độ sâu của bộ decode thì không decode được, và quay về so khớp byte sẽ mở lại cách vượt qua bằng escape | Trả về một câu cố định |
Việc che không được làm hỏng dữ liệu. So khớp chuỗi con đối với số sẽ thay thế cả những giá trị không liên quan như giá trị đếm 10, và một thông tin xác thực dài một ký tự có thể viết lại văn bản mà bộ phân loại hết thời gian chờ đọc. Số được so khớp theo giá trị chính xác, và lỗi hết thời gian chờ được phân loại dựa trên mã lỗi của cURL.
Dữ liệu cá nhân trong kết quả công cụ
Khi công cụ HTTP API gửi địa chỉ email của khách hàng, mã định danh bên ngoài hoặc các trường tùy chỉnh được đánh dấu là dữ liệu cá nhân tới upstream, các giá trị đó được che khỏi response trước khi response đến ngữ cảnh của mô hình và trace được lưu. Tên và mã định danh bản ghi của khách hàng được giữ nguyên, vì kết quả lặp lại chúng với những lý do thông thường và việc che một giá trị ngắn sẽ làm hỏng mọi lần xuất hiện của giá trị đó.
6. Phân quyền theo tác động
Một quyền gắn với trường của form có thể bỏ sót tác động mà trường đó tạo ra. Chúng tôi liệt kê những gì mà độ an toàn của một công tắc phụ thuộc vào (đích đến, payload, method), rào các thay đổi đối với những đầu vào đó trong khi công tắc đang bật, giới hạn quyền bật đối tượng và giới hạn quyền sử dụng theo yêu cầu. Chúng tôi kiểm tra hợp lệ hàng dữ liệu mà thao tác lưu sẽ giữ lại, vốn có thể khác với request đã đến.
Giới hạn quyền tại tác động
Thiết lập pre-fetch mô tả ở phần 7 biến một công cụ thành lệnh gọi mà không ai chọn. Với công cụ POST, pre-fetch còn cần một xác nhận rằng endpoint chỉ đọc. Chỉ chủ sở hữu hoặc admin của workspace mới bật được một trong hai thiết lập này, còn ai cũng có thể tắt chúng. Nếu chỉ giới hạn quyền trên các thiết lập thì vẫn còn lỗ hổng, vì một thành viên có thể giữ nguyên các thiết lập đó và đổi đích của công cụ ở bên dưới. Trong khi công cụ ở chế độ tự động, hàng rào chủ sở hữu hoặc admin cũng bao gồm các trường request của công cụ. Việc bật lại một công cụ tự động đã bị tắt cũng bị giới hạn quyền vì cùng lý do: thiết lập pre-fetch đã lưu của công cụ sẽ khôi phục các lệnh gọi tự động ngay khi công cụ được bật.
Phạm vi hiển thị bản ghi hẹp hơn phạm vi tenant
Một lượt chạy thử thủ công có thể lấy giá trị từ một cuộc hội thoại thật. Kiểm tra rằng cuộc hội thoại thuộc cùng workspace chứng minh được phạm vi tenant, trong khi hộp thư chỉ hiển thị cho mỗi người dùng những cuộc hội thoại mà họ được phép xem. Endpoint chạy thử xác định cuộc hội thoại thông qua cùng phạm vi hiển thị như hộp thư, nên người dùng không thể chỉ định cuộc hội thoại bị ẩn của đồng nghiệp để mượn thông tin khách hàng của cuộc hội thoại đó.
Kiểm tra hợp lệ hàng mà thao tác lưu sẽ giữ lại
Một lần cập nhật bỏ qua một trường sẽ giữ nguyên giá trị đã lưu của trường đó. Vì vậy, việc kiểm tra hợp lệ chỉ đọc request sẽ đánh giá một hàng không bao giờ tồn tại. Laravel không chạy quy tắc nào, kể cả quy tắc tùy chỉnh, trên thuộc tính bị thiếu hoặc là chuỗi rỗng, trừ khi đó là quy tắc implicit. Hãy xét một quy tắc liên trường chỉ cho phép admin đổi đích của công cụ gửi secret. Nếu quy tắc đọc request, một lần cập nhật một phần gửi URL mới và bỏ qua thông tin xác thực sẽ chuyển thông tin xác thực đã lưu sang một host mới.
Trước khi kiểm tra hợp lệ chạy, luồng cập nhật của chúng tôi khôi phục mọi trường đã lưu mà một quy tắc liên trường đọc tới hoặc được gắn vào, để các quy tắc đánh giá hàng đúng như khi nó được lưu. Mọi luồng lưu công cụ đều kiểm tra hợp lệ qua cùng các quy tắc đó.
7. Lệnh gọi tự động
Pre-fetch ngữ cảnh gọi các công cụ được chọn trước lượt của mô hình, để agent bắt đầu với những dữ kiện mà nếu không nó sẽ phải tra cứu hoặc đoán. Chính sách công cụ của mô hình cho phép một số thao tác ghi thật vì mô hình đã chọn lệnh gọi đó trong ngữ cảnh. Pre-fetch chạy trên mọi tin nhắn đến và chạy lại khi job được thử lại, nên một thao tác ghi sẽ lặp lại mà không ai quyết định rằng nó nên lặp lại. Vì vậy, pre-fetch có quy tắc về điều kiện đủ của riêng nó, được áp dụng khi công cụ được lưu và một lần nữa trước mỗi lệnh gọi.
Một công cụ chỉ đủ điều kiện pre-fetch khi tất cả các điều kiện sau đều đúng:
- Công cụ là GET, hoặc là POST mà chủ sở hữu hoặc admin của workspace đã xác nhận là chỉ đọc.
- Lược đồ đối số của công cụ là đóng: không có properties, không có pattern properties, không có gì bắt buộc và
additionalPropertiesđược đặt làfalse. - Không có placeholder đối số nào xuất hiện trong URL, các header được gửi hoặc body của công cụ.
POST có xác nhận tồn tại để phục vụ các API đọc nhận dữ liệu cá nhân. Nếu chuyển sang GET, số điện thoại của khách hàng sẽ đi trong URL, nơi gateway và access log lưu giữ nó.
Xác nhận khiến POST đủ điều kiện mà không hạ mức rủi ro của nó, nên các cổng kiểm soát khác của nền tảng vẫn áp dụng. Lệnh gọi bị từ chối trong sandbox chưa bật thao tác ghi ra bên ngoài, trong các lượt chạy đánh giá và trong khi cuộc hội thoại đang có một ticket hỗ trợ mở.
Pre-fetch làm việc từ một bản chụp duy nhất của lượt. Các ứng viên của pre-fetch là bản sao của các hàng công cụ trong danh mục mà mô hình nhận được cho lượt đó. Mỗi ứng viên được đối chiếu với workspace và đi qua cùng cổng kiểm soát như lệnh gọi của mô hình, nên một chỉnh sửa diễn ra giữa lượt không thể biến một GET đã có trong danh mục thành POST. Mỗi lượt có mức trần về số lượng công cụ, thời hạn cho từng lệnh gọi và cho toàn bộ bước, và ngân sách byte cho ngữ cảnh; khi ngân sách đã dùng hết, không có lệnh gọi nào khác được thực hiện.
Kết quả đến mô hình bên trong các dấu phân cách và được gắn nhãn là dữ liệu. Các cơ chế kiểm soát quan trọng không phụ thuộc vào việc mô hình tôn trọng nhãn đó: danh mục của lượt, các vị trí mà mô hình có thể điền và mọi giá trị do nền tảng cung cấp đều được cố định bởi cấu hình mà mô hình không thể chỉnh sửa.
8. Giá trị của mô hình, workspace và nền tảng
Mô hình chọn đối số. Nền tảng điền các giá trị như số điện thoại của khách hàng hiện tại. Hai nguồn này không bao giờ được chồng lấn, vì khi đó một mô hình bị prompt injection sẽ chọn được lệnh tra cứu dùng dữ liệu của ai.
- Xung đột lược đồ. Lược đồ công cụ có đối số trùng tên với một trường hội thoại hoặc trường liên hệ do nền tảng điền sẽ bị từ chối lúc lưu và một lần nữa khi công cụ được dựng cho một lượt. Secret đã lưu không bao giờ được đọc từ đối số, nên không lược đồ nào có thể đứng thay cho một secret.
- Phạm vi workspace. Giá trị của liên hệ, cùng số điện thoại hoặc username trên danh tính kênh của cuộc hội thoại, chỉ được đọc khi chúng thuộc về workspace của cuộc hội thoại.
- Giá trị danh tính mang theo nền tảng của nó. Username chỉ là duy nhất trong ứng dụng nhắn tin của chính nó, nên công cụ chỉ nhận username của cuộc hội thoại khi danh tính nằm trên một nền tảng mà ở đó handle định danh được người gửi. Khi người gửi bỏ username, danh tính sẽ xóa username đó, vì người khác có thể nhận nó sau này. Một tin nhắn cũ hơn được xử lý muộn không thể khôi phục username: danh tính giữ username từ tin nhắn mới nhất của nó, với phép so sánh diễn ra bên trong một lệnh cập nhật cơ sở dữ liệu duy nhất.
- Tên dành riêng. Công cụ của workspace hoặc endpoint từ xa được đặt tên giống công cụ của nền tảng sẽ che khuất công cụ đó hoặc bị loại bỏ. Mọi tên công cụ mà nền tảng có thể cung cấp trong hội thoại với khách hàng đều được dành riêng, và một bài kiểm thử kiểm kê sẽ thất bại khi một công cụ mới của nền tảng không có trong danh sách. Các mẫu tên kết thúc bằng
\z, vì trong PCRE$còn khớp cả trước ký tự xuống dòng cuối cùng.
Quy tắc lược đồ của nhà cung cấp là một phần của đặc tả
Quy tắc lược đồ của nhà cung cấp mô hình quyết định liệu request có được chấp nhận hay không. Một object JSON rỗng khi decode sẽ thành mảng PHP rỗng, và object có key dạng số khi decode sẽ thành list. Khi được encode lại, lược đồ như vậy đến nhà cung cấp với một array ở chỗ yêu cầu object. Nhà cung cấp nào bắt buộc kiểu dữ liệu, như function declaration của Gemini, sẽ từ chối toàn bộ request, và mọi lượt hội thoại có chứa công cụ đó đều thất bại. Chúng tôi kiểm tra hợp lệ lược đồ của endpoint từ xa bằng một bộ duyệt biết giá trị của từng keyword JSON Schema chứa gì.
9. Giới hạn tài nguyên
Mỗi tài nguyên cần một giới hạn tại nơi nó bị tiêu thụ. Phép kiểm tra kích thước sau khi body đã đến, lệnh tra cứu DNS không có thời hạn và thời gian chờ bằng 0 đều cho phép khối lượng công việc không giới hạn.
| Tài nguyên | Dạng lỗi | Cơ chế kiểm soát |
|---|---|---|
| Số byte của response | Kiểm tra kích thước sau khi client trả về khiến việc truyền tải không bị giới hạn: cURL handler của Guzzle đã ghi toàn bộ body vào một stream tạm | Một sink cho response từ chối ghi vượt quá mức trần cố định ngay khi byte đến; giới hạn kích thước riêng của cURL được giữ làm giới hạn thứ hai |
| Thời gian | Trong Guzzle, thời gian chờ bằng 0 nghĩa là chờ vô thời hạn, và đó là giá trị mặc định | Thời gian chờ của công cụ phải nằm trong khoảng 1 đến 120 giây, và giá trị dưới một giây bị từ chối lúc chạy |
| DNS | dns_get_record() không có thời gian chờ |
Phân giải trong một tiến trình bị dừng tại một thời hạn nằm trong ngân sách của lệnh gọi |
| Danh mục công cụ | Mọi công cụ đang bật được mô tả cho mọi agent tiếp xúc với khách hàng ở mọi lượt, và nhà cung cấp đặt mức trần cho số function declaration mỗi request | Tối đa 32 công cụ API của workspace trong danh mục của một lượt, và 16,384 ký tự cho mỗi lược đồ |
Mức trần của danh mục được xác định theo giới hạn đã công bố thận trọng nhất của các nhà cung cấp. Tài liệu tham chiếu API của OpenAI nêu mức tối đa 128 function cho tools của Chat Completions ít nhất cho đến tháng 4 năm 2025, và tài liệu tham chiếu function calling của Google đưa ra con số 128 function declaration mỗi request. Với 32 công cụ của workspace, toàn bộ danh mục của agent StaffOS lớn nhất vẫn dưới 128.
10. Tính toàn vẹn của đầu ra
Các cơ chế kiểm soát tính bí mật không bao quát mọi tác hại mà một công cụ có thể gây ra. Khi API trả về danh sách bản ghi với các trường song song, mô hình có thể ghép tên của bản ghi này với nhãn của bản ghi khác và nói với khách hàng một điều sai. Cơ chế render response của chúng tôi có thể gộp mỗi bản ghi thành một dòng văn bản trước khi mô hình nhìn thấy, nên các trường của một bản ghi đến mô hình khi đã được ghép lại với nhau.
Render theo dòng, khi thất bại, sẽ quay về dữ liệu gốc. Bản ghi thiếu một trường được tham chiếu sẽ được giữ nguyên vẹn, và template không parse được sẽ để payload nguyên như cũ. Cơ chế render từ chối viết lại một object tại đường dẫn đã cấu hình thành list, và đường dẫn cùng template được lưu cùng nhau, hoặc không cái nào được lưu.
11. Cách chúng tôi xây dựng và kiểm chứng các cơ chế kiểm soát bảo mật
Các thay đổi ở những ranh giới này được xây dựng và kiểm tra như sau.
- Rà soát đối kháng. Một agent rà soát tự động, được chỉ dẫn để phá vỡ thay đổi, rà soát từng bản sửa đổi và thường cung cấp cách tái hiện kèm theo mỗi phát hiện. Tác giả tái hiện từng nhận định trước khi sửa mã.
- Guard được kiểm tra bằng đột biến. Mỗi guard mới được gỡ bỏ hoặc vô hiệu hóa trong một bản sao mã dùng một lần, và thay đổi chỉ được tính khi sau đó có một bài kiểm thử thất bại. Bài kiểm thử vẫn đạt khi không có guard của nó sẽ được viết lại cho đến khi nó không thể đạt nữa. Phương pháp này được trình bày trong bài báo năm 1978 của DeMillo, Lipton và Sayward về lựa chọn dữ liệu kiểm thử.
- Ưu tiên bảng đặc tả hơn ví dụ. Mỗi đặc tả là một bảng các lớp đầu vào, và mỗi hàng khẳng định chính xác các byte được gửi hoặc khẳng định rằng không có gì được gửi:
- địa chỉ dành cho mục đích đặc biệt, một bảng dùng chung cho unit test của chính sách địa chỉ và các phép kiểm tra lúc lưu và lúc gửi;
- các dạng URL, chạy qua lệnh gọi của mô hình và pre-fetch;
- các vị trí trong body và header, chạy qua lệnh gọi của mô hình và mọi luồng chạy thử thủ công;
- các header mà connector không bao giờ gửi, kết hợp chéo với các đoạn văn bản sẽ khiến lệnh gọi bị từ chối nếu được render;
- cô lập thông tin xác thực, kết hợp chéo cả hai loại công cụ với mọi vị trí thông tin xác thực có thể nằm và mọi loại lỗi;
- các thao tác lưu, trên mọi luồng lưu công cụ, các trạng thái ban đầu và các dạng đầu vào của nó.
- Mỗi quy tắc một bản cài đặt. Một quy tắc được kiểm tra lúc lưu và lúc chạy là một hàm được gọi hai lần. Hai cách suy ra cùng một điều kiện sẽ dần lệch khỏi nhau.
- Fail closed với những gì không đọc được. Chế độ xác thực không xác định, thông tin xác thực không giải mã được, host không phân giải được, body JSON lồng vượt giới hạn của bộ decode và dạng mã mà guard tĩnh không parse được đều dẫn đến từ chối.
12. Đối chiếu với các hệ phân loại công khai
Phần đối chiếu này do chúng tôi thực hiện để định hướng. Phần đối chiếu tránh dùng CWE-200 và CWE-269, hai mục mà MITRE khuyến cáo không nên dùng để ánh xạ lỗ hổng.
| Nhóm lỗi | CWE | OWASP Top 10 for LLM Applications 2025 | OWASP API Security Top 10 2023 |
|---|---|---|---|
| Kiểm soát đích đến chiều đi | CWE-918 Server-Side Request Forgery; CWE-367 Time-of-check Time-of-use Race Condition, cho DNS rebinding | API7:2023 Server Side Request Forgery | |
| Dựng request | CWE-180 Incorrect Behavior Order: Validate Before Canonicalize; CWE-116 Improper Encoding or Escaping of Output; CWE-235 Improper Handling of Extra Parameters | LLM05:2025 Improper Output Handling | |
| Phạm vi secret | CWE-441 Unintended Proxy or Intermediary ("Confused Deputy"); CWE-863 Incorrect Authorization; CWE-532 Insertion of Sensitive Information into Log File | LLM02:2025 Sensitive Information Disclosure | |
| Cô lập | CWE-209 Generation of Error Message Containing Sensitive Information; CWE-532 | LLM02:2025 | API10:2023 Unsafe Consumption of APIs |
| Phân quyền theo tác động | CWE-863; CWE-639 Authorization Bypass Through User-Controlled Key, cho phạm vi hiển thị bản ghi | API1:2023 Broken Object Level Authorization; API3:2023 Broken Object Property Level Authorization; API5:2023 Broken Function Level Authorization | |
| Lệnh gọi tự động | CWE-863; CWE-367, cho việc đọc lại cấu hình giữa lượt | LLM06:2025 Excessive Agency | |
| Giá trị của mô hình và nền tảng | CWE-639, cho khóa tra cứu mà nếu không mô hình có thể chọn; CWE-441; CWE-116; CWE-625 Permissive Regular Expression | LLM01:2025 Prompt Injection; LLM05:2025 | API1:2023 Broken Object Level Authorization |
| Giới hạn tài nguyên | CWE-770 Allocation of Resources Without Limits or Throttling | LLM10:2025 Unbounded Consumption | API4:2023 Unrestricted Resource Consumption |
| Tính toàn vẹn của đầu ra | LLM09:2025 Misinformation |
Với các đội ngũ xây dựng trên MCP, các thực hành bảo mật tốt nhất của Model Context Protocol đề cập các kiểu tấn công liên quan trong bối cảnh riêng của giao thức đó, bao gồm tấn công confused deputy, token passthrough và SSRF.
Phụ lục: ghi chú triển khai
Chi tiết dành cho các đội ngũ xây dựng cùng những cơ chế kiểm soát này.
Lượt chạy thử và chuẩn hóa đầu vào
Laravel cắt khoảng trắng ở đầu vào dạng chuỗi và chuyển chuỗi rỗng thành null trong middleware mặc định của nó. Route chạy thử thủ công được miễn trừ đối với giá trị đối số, nên một lượt chạy thử render chính xác các byte mà lệnh gọi của mô hình sẽ render.
Đầu vào form được flash
Khi form thiết lập không qua kiểm tra hợp lệ, đầu vào được flash lại vào session sẽ bỏ qua các trường thông tin xác thực của cả hai loại công cụ: URL, header, xác thực và body của công cụ HTTP API, giá trị secret đã lưu, access token, và URL, header, xác thực của từng endpoint từ xa trong một danh sách. Các trường này được gỡ bỏ sau bất kỳ thành phần nào đã flash đầu vào, nên một danh sách dùng tên làm key hoặc một controller tự flash đầu vào cũng được bao quát.
Token phiên bản
Các hàng công cụ chứa thông tin xác thực mang một token phiên bản để khóa lạc quan. Một digest không có salt của hàng sẽ cho phép bất kỳ ai giữ token thử các phỏng đoán về thông tin xác thực trong hàng đó theo kiểu offline, nên token được tính trên hàng cùng với một secret của máy chủ. Token cũng được tính với JSON_THROW_ON_ERROR: json_encode() trả về false khi gặp UTF-8 không hợp lệ, và nếu không có cờ này, mọi hàng như vậy sẽ dùng chung một token và vượt qua mọi phép kiểm tra dữ liệu cũ.
Đo lược đồ theo một cách duy nhất
Giới hạn lược đồ được đo theo cùng một cách lúc lưu và lúc chạy. Encode JSON mà không có JSON_UNESCAPED_UNICODE sẽ lưu mỗi ký tự không phải ASCII thành một chuỗi escape \uXXXX dài sáu ký tự, nên đo văn bản đã lưu sẽ đếm một lược đồ tiếng Trung nhiều gấp khoảng sáu lần, còn đo văn bản đã gửi lên sẽ đếm cả phần thụt lề của form thiết lập. Một hàm duy nhất phục vụ cả hai phép kiểm tra: hàm này đo lược đồ ở dạng viết gọn, với mỗi ký tự được đếm đúng là chính nó.
Thay thế một danh sách
Thao tác ghi cung cấp một danh sách endpoint từ xa sẽ thay thế toàn bộ danh sách đã lưu. array_replace_recursive() gộp danh sách theo chỉ số, nên việc gộp sẽ giữ lại những mục mà chủ sở hữu đã xóa, cùng với thông tin xác thực đã lưu của các mục đó.
Độ dài được khai báo của một response 304
RFC 9110 §8.6 cho phép một response 304 mang Content-Length mà một response 200 lẽ ra sẽ có, dù 304 không chứa body. Phép kiểm tra kích thước response đo body nhận được và không bao giờ đọc độ dài được khai báo.
Tác giả và cách trích dẫn
Vin Lim là Giám đốc công nghệ của StaffOS. Anh làm việc về điều phối agent, tích hợp hệ thống doanh nghiệp và bảo mật cho các công cụ mà agent AI gọi.
Cách trích dẫn đề xuất: Lim, V. (2026). Security engineering for AI agent connectors: nine failure classes. Tài liệu kỹ thuật StaffOS, phiên bản 1.0, ngày 24 tháng 9. https://staffos.xyz/blog/ai-agent-connector-security
Để tìm hiểu cách chúng tôi tổ chức và đánh giá các agent sử dụng những công cụ này, xem bài viết của chúng tôi về kỹ thuật harness cho agent.
Frequently asked questions
Vì sao lệnh gọi công cụ của agent AI là một rủi ro bảo mật? +
Một agent gọi API bên ngoài có thể đọc dữ liệu riêng tư, đọc nội dung chưa ai kiểm duyệt và có thể gửi dữ liệu ra ngoài. Simon Willison gọi tổ hợp đó là bộ ba chết người. Nếu phần mềm bao quanh mô hình không kiểm soát đích đến, nội dung của request và các response, một prompt bị thao túng hoặc một công cụ cấu hình sai có thể đưa dữ liệu đến nơi nó không được phép đến.
Làm sao ngăn SSRF khi khách hàng tự cấu hình URL API của mình? +
Chỉ chấp nhận hostname thuần và địa chỉ IP ở dạng chuẩn. Dùng một bảng dải địa chỉ tường minh để từ chối mọi dải mà các sổ đăng ký địa chỉ dành cho mục đích đặc biệt của IANA đánh dấu là không thể truy cập toàn cục, bao gồm không gian loopback, riêng, dùng chung và link-local cùng địa chỉ metadata của cloud. Phân giải tên trong một thời hạn và ghim địa chỉ đã kiểm tra vào kết nối để câu trả lời DNS thứ hai không thể thay đổi nó. Tắt chuyển hướng và các thiết lập proxy kế thừa từ môi trường.
Vì sao kiểm tra URL trước khi gửi là chưa đủ? +
URL mà guard kiểm tra có thể khác URL được gửi đi. Một số HTTP client mở rộng mọi URL như một template RFC 6570 sau khi ứng dụng dựng URL đó, cURL kết nối tới những cách viết host mà parser nghiêm ngặt không nhận ra là địa chỉ, và proxy tự phân giải tên. Một phép kiểm tra chỉ có hiệu lực khi nó đánh giá đúng đầu vào mà tầng truyền tải sẽ dùng.
Nên ghi đối số của mô hình vào body JSON của request như thế nào? +
Ghi dưới dạng JSON, theo vị trí mà mỗi đối số chiếm. Bên trong một chuỗi JSON, ghi đối số dưới dạng nội dung chuỗi, chỉ escape ở những chỗ JSON yêu cầu. Tại vị trí giá trị, ghi giá trị JSON mà mô hình đã gửi. Percent-encoding khiến upstream đọc nguyên văn phần văn bản đã encode, còn dán văn bản thô vào thì một dấu ngoặc kép có thể thêm key. Từ chối body không phải JSON hợp lệ sau khi mọi giá trị đã được ghi vào.
Đối số của mô hình có thể thay đổi endpoint API mà công cụ gọi không? +
Không nên có khả năng đó. Percent-encode mọi đối số được ghi vào URL, từ chối mọi cú pháp template còn sót lại, và từ chối giá trị tạo thành segment . hoặc .. trong đường dẫn. libcurl loại bỏ các dot segment trước khi gửi request, nên với một template như /customers/{id}/orders và id là .., request sẽ đến /orders trên host của người vận hành, mang theo thông tin xác thực của công cụ.
Nền tảng agent AI nên giới hạn phạm vi secret của API như thế nào? +
Giữ secret đã lưu của mỗi workspace trong kho mã hóa riêng của workspace đó và chỉ phân giải tham chiếu từ kho đó. Sau đó giới hạn phạm vi secret đã lưu theo cách sử dụng: chỉ những người được phép quản lý một secret mới có thể thay đổi nơi request mang secret đó được gửi đến, chạy thử request đó hoặc bật các lệnh gọi tự động gửi secret đó.
Làm sao giữ thông tin xác thực nằm ngoài những gì mô hình nhìn thấy? +
Thay nội dung lỗi của tầng truyền tải bằng một phân loại và một câu cố định, vì lỗi của thư viện HTTP có thể trích dẫn URL, hostname và giá trị header. Với response của công cụ, che các thông tin xác thực mà công cụ đã gửi ở mọi cách viết JSON cho phép, và giữ lại body JSON lồng quá sâu đến mức không decode được.
Mô hình có thể chọn khách hàng mà công cụ tra cứu không? +
Chỉ thông qua các đối số mà lược đồ của công cụ khai báo. Nền tảng điền số điện thoại của cuộc hội thoại và thông tin của liên hệ, và lược đồ khai báo một đối số mang một trong các tên đó sẽ bị từ chối khi công cụ được lưu và một lần nữa khi công cụ được dựng cho một lượt. Secret đã lưu hoàn toàn không bao giờ được đọc từ đối số.
References
- [1] Willison, S. The lethal trifecta for AI agents: private data, untrusted content, and external communication. 16 June 2025.
- [2] OWASP Cheat Sheet Series. Server-Side Request Forgery Prevention Cheat Sheet.
- [3] IANA. IPv4 Special-Purpose Address Registry.
- [4] IANA. IPv6 Special-Purpose Address Registry.
- [5] Alibaba Cloud documentation. View instance metadata (metadata service at 100.100.100.200).
- [6] OWASP. Top 10 for LLM Applications 2025.
- [7] OWASP. API Security Top 10 2023.
- [8] Model Context Protocol. Security Best Practices.
- [9] IETF RFC 6570. URI Template. Sections 3.2.1, 3.2.3 and 3.2.4.
- [10] IETF RFC 3986. Uniform Resource Identifier: Generic Syntax. Section 5.2.4, Remove Dot Segments.
- [11] IETF RFC 8259. The JSON Data Interchange Format. Section 7.
- [12] IETF RFC 9110. HTTP Semantics. Section 8.6, Content-Length.
- [13] Laravel framework source. PendingRequest::send() expands every URL through UriTemplate::expand().
- [14] Laravel documentation. HTTP Client, URI Templates.
- [15] Laravel documentation. Requests, Input Trimming and Normalization.
- [16] Laravel documentation. Validation, Implicit Rules.
- [17] Guzzle uri-template source. UriTemplate::expand().
- [18] Guzzle source. Client reads HTTP_PROXY, HTTPS_PROXY and NO_PROXY from the environment.
- [19] Guzzle documentation. Request options: allow_redirects and timeout.
- [20] Requests documentation. Proxies.
- [21] Requests documentation. Redirection and History.
- [22] Go standard library. net/http: DefaultTransport, ProxyFromEnvironment and Client.CheckRedirect.
- [23] Node.js documentation. NODE_USE_ENV_PROXY.
- [24] MDN Web Docs. RequestInit: redirect.
- [25] curl documentation. CURLOPT_RESOLVE.
- [26] curl documentation. CURLOPT_PATH_AS_IS.
- [27] curl documentation. CURLOPT_PROXY.
- [28] PHPWord source. Word2007 reader loads an external VML image from the URL the document names.
- [29] PHP manual. parse_str.
- [30] PHP manual. strtr.
- [31] PHP manual. PCRE anchors.
- [32] Meta for Developers. Onboarding business customers as a Tech Provider (Embedded Signup token exchange).
- [33] OpenAI openai-openapi specification, commit 498c71d. Chat Completions tools: "A max of 128 functions are supported."
- [34] Google Cloud. Function calling reference, limitations.
- [35] Hardy, N. The Confused Deputy (or why capabilities might have been invented). ACM SIGOPS Operating Systems Review 22(4), 1988.
- [36] DeMillo, R. A., Lipton, R. J., Sayward, F. G. Hints on Test Data Selection: Help for the Practicing Programmer. IEEE Computer 11(4), 1978.
- [37] MITRE. CWE-918: Server-Side Request Forgery (SSRF).
- [38] MITRE. CWE-639: Authorization Bypass Through User-Controlled Key.
About the author
Vin Lim
Đồng sáng lập / CTO, StaffOS
Vin Lim là Giám đốc công nghệ của StaffOS. Công việc của anh bao gồm điều phối agent, tích hợp hệ thống doanh nghiệp và các cơ chế kiểm soát những gì agent AI có thể truy cập, gửi đi và tiết lộ.
Related reading
Kỹ thuật harness cho agent: cải thiện AI mà không tinh chỉnh mô hình
Cách StaffOS thiết kế ngữ cảnh, công cụ và cơ chế thực thi khi giữ nguyên trọng số mô hình. Kết quả nghiên cứu công khai, tình huống giả lập và biểu đồ có thể tái tạo.
Speed-to-lead trong kỷ nguyên agent: đường cong HBR vẫn đúng, sàn giờ tính bằng giây.
Đường cong speed-to-lead của HBR năm 2011 vẫn đúng, nhưng thời gian phản hồi trung vị của ngành đã tệ đi. Đây là điều AI SDR thay đổi và vì sao sàn giờ tính bằng giây.
Agent AI khiến drip cadence trở nên lỗi thời. Nuôi dưỡng theo tín hiệu là cái thay thế nó.
Drip cadence được tạo ra khi cá nhân hóa rất đắt và chi phí gửi tin nhắn gần như bằng không. LLM đã đảo ngược nền kinh tế đó. Đây là lập luận cho nuôi dưỡng theo tín hiệu trong kỷ nguyên agent.