Rust Canister 开发安全最佳实践
身份验证
确保只有特定用户才能执行的任何操作都需要身份验证
推荐
-
根据设计,对于每个容器调用,都可以识别调用者。 调用principal 可以使用系统API的方法`ic0.msg_caller_size`和`ic0.msg_caller_copy`访问(见这里)。 如果例如 使用Internet Identity,principal是这个特定来源的用户身份,见link:https://github.com/dfinity/internet-identity/blob/main/docs/internet-identity-spec.adoc#identity-design -和数据模型[这里]。 如果某些操作(例如访问用户帐户数据或帐户特定操作)应限制为一个主体或一组主体,则必须在容器调用中明确检查,例如 Rust 中的如下:
// Let pk be the public key of a principal that is allowed to perform
// this operation. This pk could be stored in the canister's state.
if caller() != Principal::self_authenticating(pk) { ic_cdk::trap(...) }
// Alternatively, if the canister keeps data for different principals
// in e.g. a map such as BTreeMap<Principal, UserData>, then the canister
// must ensure that each caller can only access and perform operations
// on their own data:
if let Some(user_data) = user_data_store.get_mut(&caller()) {
// perform operations on the user's data
}
-
在 Rust 中,
ic_cdk
crate 可用于使用ic_cdk::api::caller
对调用者进行身份验证。 确保返回的主体是Principal::self_authenticating
类型,并使用该主体的公钥识别用户帐户,请参见上面的示例代码。 -
在调用中尽早进行身份验证,以避免在身份验证之前进行未经身份验证的操作和潜在的昂贵操作。 拒绝向匿名用户提供服务 也是一个好主意。
在经过身份验证的调用中禁止匿名主体
安全问题
ic0::api::caller
也可能返回 Principal::anonymous()
。 在经过身份验证的呼叫中,这可能是不希望的(并且可能具有安全隐患),因为对于进行未经身份验证的呼叫的任何人来说,这就像一个共享帐户。
推荐
在经过身份验证的调用中,确保调用者不是匿名的,如果是则返回错误或陷阱。 这可以例如 通过使用辅助方法集中完成,例如:
fn caller() -> Result<Principal, String> {
let caller = ic0::api::caller();
// The anonymous principal is not allowed to interact with canister.
if caller == Principal::anonymous() {
Err(String::from(
"Anonymous principal not allowed to make calls.",
))
} else {
Ok(caller)
}
}
资产认证
使用 HTTP 资产认证并避免通过 raw.ic0.app
为您的 dApp 提供服务
安全问题
IC 上的 dApp 可以使用资产认证 来确保交付给浏览器的 HTTP 资产是真实的(即由子网进行阈值签名)。 如果一个应用程序不做资产认证,它只能通过不检查资产认证的 raw.ic0.app
不安全地提供服务。 这是不安全的,因为单个恶意节点或边界节点可以自由修改传递给浏览器的资产。
如果除 ic0.app
之外还通过 raw.ic0.app
提供应用程序,则攻击者可能会诱骗用户(网络钓鱼)使用不安全的 raw.ic0.app。
推荐
-
仅通过服务工作者验证资产认证的
<canister-id>.ic0.app
提供资产。 不要通过<canister-id>.raw.ic0.app
提供服务。 -
使用资产容器(自动创建资产认证)提供资产,或添加包括资产认证的“ic-certificate”标头,例如 完成NNS dApp 或Internet Identity。
-
如果请求来自原始,请检查容器的
http_request
方法。 如果是这样,则返回错误并且不提供任何资产。
容器存储
考虑使用稳定的内存,版本,测试
安全问题
容器内存不会在升级过程中保持不变。 如果需要在升级期间保留数据,很自然的做法是在 pre_upgrade
中序列化容器内存,并在 post_upgrade
中对其进行反序列化。 但是,这些方法的可用指令数量是有限的。 如果内存增长过大,则无法再更新容器。
推荐
-
稳定内存在升级过程中保持不变,可用于解决此问题。
-
Consider using stable memory. (from Effective Rust Canisters). See also the disadvantages discussed there.
-
See also the section on upgrades in How to audit an Internet Computer canister (though focused on Motoko)
-
Write tests for stable memory to avoid bugs.
-
Some libraries (mostly work in progress / partly unfinished) that people work on:
-
请参阅Internet 计算机的当前限制,“长期运行升级”和“需要额外 wasm 内存的 [de]serializer”部分
-
例如internet identity 直接使用稳定内存存储用户数据。
创建备份
推荐
-
确保升级中使用的方法经过测试,否则容器变得不可变。
-
制定可以重新安装容器的灾难恢复策略可能会很有用。
-
请参阅“备份和恢复”部分How to audit an Internet Computer canister
容器间调用和回滚
不要在等待之后恐慌,不要跨等待边界锁定共享资源
安全问题
恐慌和陷阱回滚罐状态。因此,任何伴随陷阱或恐慌的状态变化都是值得关注的。当进行容器间调用时,这也是一个重要的问题。如果在容器间调用的“等待”之后发生恐慌/陷阱,则状态将恢复到容器间调用回调调用之前的快照(而不是在整个调用之前!)。
这可能是例如导致以下问题:
-
如果在容器间调用之前状态更改导致状态不一致,并且在容器间调用之后出现恐慌,则会导致容器状态不一致。
-
特别是,如果在容器间调用之前分配的资源(例如锁或内存)没有被释放,这可以例如导致罐子被永远锁定。
-
通常,当开发人员预期的数据未持久化时,可能会出现错误。
推荐
-
Don’t lock shared resources across await boundaries (from Effective Rust Canisters)
-
See also: "Inter-canister calls" section in How to audit an Internet Computer canister
-
For context: IC interface spec on message execution
请注意,容器间调用期间状态可能会发生变化
安全问题
消息(但不是整个调用)以原子方式处理。 这可能会导致安全问题,例如:
-
Time-of-check time-of-use:在容器间调用之前检查全局状态的某些条件,并错误地假设它在调用返回时仍然保持。
推荐
-
请注意,在容器间调用期间状态可能会发生变化。 仔细检查您的代码,以免出现此类错误。
-
另请参阅:“容器间调用”部分How to audit an Internet Computer canister
仅对可信赖的容器进行容器间调用
安全问题
-
如果对潜在的恶意容器进行容器间调用,这可能会导致 DoS 问题,或者可能存在与坦率解码相关的问题。 此外,可以假定从容器调用返回的数据是可信的,但实际上并非如此。
-
如果使用回调调用容器,如果对等方没有响应,接收方可能会无限期停止,从而导致 DoS。 如果容器处于该状态,则无法再升级它。 恢复将涉及重新安装、擦除容器的状态。
-
总之,如果容器的行为取决于容器间调用响应,这可能会拒绝容器、消耗过多的资源或导致逻辑错误。
推荐
-
仅对可信赖的容器进行容器间调用。
-
清理从容器间调用返回的数据。
-
请参阅“与恶意罐交谈”部分:linkhttps://www.joachim-breitner.de/blog/788-How_to_audit_an_Internet_Computer_canister[How to audit an Internet Computer canister]
-
See Current limitations of the Internet Computer, section "Calling potentially malicious or buggy canisters can prevent canisters from upgrading"
确保调用图中没有循环
推荐
-
避免这样的循环!
-
有关更多信息,请参阅Current limitations of the Internet Computer, section "Loops in call graphs"
罐升级
在升级过程中小心恐慌
推荐
-
避免在
pre_upgrade
挂钩中出现恐慌/陷阱,除非它确实不可恢复,因此任何无效状态都可以通过升级修复。 pre-upgrade hook 中的 panic 会阻止升级,并且由于 pre-upgrade hook 由旧代码控制,它可以永久阻止升级。 -
如果状态无效,则在
post_upgrade
挂钩中出现恐慌,以便可以重试升级并尝试修复无效状态。 升级后挂钩中的恐慌会中止升级,但可以使用新代码重试。 -
另请参阅有关升级的部分如何审核互联网计算机容器(尽管侧重于 Motoko)
-
参见互联网计算机的当前限制,“
pre_upgrade
挂钩中的错误”部分
杂项
即使存在系统 API 调用,也可以测试您的容器代码
推荐
-
创建不依赖于系统 API 的松散耦合模块并对它们进行单元测试。 看到这个recommendation (from Effective Rust Canisters).
-
对于仍与系统 API 交互的部分:创建在单元测试中伪造的系统 API 的精简抽象。 见recommendation (from Effective Rust Canisters).例如,可以按如下方式实现“运行时”,然后在测试中使用“模拟运行时”(Dimitris Sarlis 编写的代码):
use ic_cdk::api::{
call::call, caller, data_certificate, id, print, time, trap,
};
#[async_trait]
pub trait Runtime {
fn caller(&self) -> Result<Principal, String>;
fn id(&self) -> Principal;
fn time(&self) -> u64;
fn trap(&self, message: &str) -> !;
fn print(&self, message: &str);
fn data_certificate(&self) -> Option<Vec<u8>>;
(...)
}
#[async_trait]
impl Runtime for RuntimeImpl {
fn caller(&self) -> Result<Principal, String> {
let caller = caller();
// The anonymous principal is not allowed to interact with the canister.
if caller == Principal::anonymous() {
Err(String::from(
"Anonymous principal not allowed to make calls.",
))
} else {
Ok(caller)
}
}
fn id(&self) -> Principal {
id()
}
fn time(&self) -> u64 {
time()
}
(...)
}
pub struct MockRuntime {
pub caller: Principal,
pub canister_id: Principal,
pub time: u64,
(...)
}
#[async_trait]
impl Runtime for MockRuntime {
fn caller(&self) -> Result<Principal, String> {
Ok(self.caller)
}
fn id(&self) -> Principal {
self.canister_id
}
fn time(&self) -> u64 {
self.time
}
(...)
}
使容器构建可重现
推荐
使容器构建可重现。 看到这个recommendation (from Effective Rust Canisters). See also Developer docs on this.
不要依赖时间是严格单调的
推荐
请参阅“时间不是严格单调的”部分如何审核 Internet 计算机容器
防止耗尽循环余额
推荐
考虑监控、早期身份验证、容器级别的速率限制以减轻这种情况。另外,请注意,攻击者的目标是消耗大多数燃料费的调用。请参阅“循环平衡消耗攻击部分”如何审核 Internet 计算机容器。
非特定于 Internet 计算机
本节中的最佳实践非常笼统,并不特定于 Internet 计算机。此列表绝不是完整的,仅列出了过去导致问题的一些非常具体的问题。
验证输入
安全问题
发送的数据查询和更新调用一般是不可信的。消息大小限制为几 MB。这可以例如引导以下问题:
-
如果未经验证的数据在 Web UI 中呈现或显示在其他系统中,这可能导致注入攻击(例如 XSS)。
-
大尺寸的消息可能会被发送并可能存储在容器中,从而消耗过多的存储空间。
-
大输入(例如大列表或字符串)可能会触发过多的计算,导致 DoS 并消耗许多燃料费。另见防止耗尽燃料费余额
推荐
-
执行输入验证,参见例如OWASP备忘单。
-
“大数据攻击”部分如何审核 Internet 计算机容器(注意坦率的太空炸弹)
-
ASVS 5.1.4:验证结构化数据是强类型的,并针对定义的模式进行验证,包括允许的字符、长度和模式(例如信用卡号码或电话,或验证两个相关字段是否合理,例如检查郊区和邮政编码匹配)。
Rust:避免整数溢出
安全问题
Rust 中的整数可能会溢出。虽然这样的溢出会导致调试配置出现恐慌,但这些值只是在发布编译中默默地包裹起来。这可能会导致重大的安全问题,例如当整数用作索引、唯一 ID 或计算燃料费期或 ICP 量时。
推荐
-
仔细检查您的代码是否存在任何可能环绕的整数运算。
-
使用这些操作的“饱和”或“已检查”变体,例如“饱和添加”、“饱和订阅”、“检查添加”、“检查订阅”等。参见例如Rust docs for
u32
。
对于昂贵的调用,考虑使用验证码或工作证明
推荐
如果 dApp 提供此类操作,请考虑使用 bot 预防技术,例如添加验证码或工作量证明。有例如internet identity中的验证码实现。