Rust Canister 开发安全最佳实践

不可变的智能合约

使用像SNS这样的去中心化治理系统,让canister拥有去中心化的控制器

安全问题

容器的控制者可以随时改变/更新容器。如果一个容器储存了ICP等资产,这实际上意味着控制者可以通过更新容器来窃取这些资产,并将燃料费转移到他们的账户。

建议

  • 要实现不可变的容器智能合约,你不能成为容器的控制者。控制权必须传递给服务神经系统(SNS)或一个去中心化的治理系统。

  • 创建不可变的容器智能合约的另一个选择,是完全删除容器控制器。但请注意,这意味着容器不能被升级,这可能会产生严重的影响,例如发现一个错误。

  • 请注意,与其他一些区块链相反,不可变的智能合约也需要燃料费来运行,而且它们可以接收燃料费。

验证您所依赖的智能合约的不变性

安全问题

如果容器依赖容器智能合约(即对其进行容器间调用),容器智能合约必须是不可变的。 否则,即如果它有一个控制器,他们可以随时修改智能合约,例如 窃取容器持有的资产。

推荐

如果您与必须不可变智能合约的容器进行交互,请确保它由 NNS、服务神经系统 (SNS) 或去中心化治理系统控制。

身份验证

确保只有特定用户才能执行的任何操作都需要身份验证

安全问题

如果不是这种情况,攻击者可能能够代表用户执行敏感操作,从而危及他们的帐户。

推荐

  • 根据设计,对于每个容器调用,都可以识别调用者。 调用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 dAppInternet Identity

  • 如果请求来自原始,请检查容器的 http_request 方法。 如果是这样,则返回错误并且不提供任何资产。

容器存储

使用 thread_local!Cell/RefCell 作为状态变量,并将所有全局变量放在一个篮子中。

安全问题

容器需要全局可变状态。 在 Rust 中,有几种方法可以实现这一点。 但是,某些选项可能会导致,例如 到内存损坏。

限制每个用户可以存储在容器中的数据量

安全问题

如果用户能够在容器上存储大量数据,这可能会被滥用以填满容器存储并使容器无法使用。

推荐

限制每个用户可以存储在容器中的数据量。 每当在更新调用中为用户存储数据时,都必须检查此限制。

考虑使用稳定的内存,版本,测试

安全问题

容器内存不会在升级过程中保持不变。 如果需要在升级期间保留数据,很自然的做法是在 pre_upgrade 中序列化容器内存,并在 post_upgrade 中对其进行反序列化。 但是,这些方法的可用指令数量是有限的。 如果内存增长过大,则无法再更新容器。

推荐

考虑加密容器上的敏感数据

安全问题

默认情况下,容器提供完整性但不提供机密性。 存储在容器上的数据可以被节点/副本读取。

推荐

  • 考虑对容器上的任何私人或个人数据(例如用户的个人或私人信息)进行端到端加密。

  • 我们目前正在开发一个示例 dApp(加密笔记)来说明如何进行端到端加密。

创建备份

安全问题

容器可能会变得不可用,因此它永远无法再次升级,例如 由于以下原因:

  • 它有一个错误的升级过程(由于 dapp 开发人员的一些错误)。

  • 由于持久化数据的代码中的错误,状态变得不一致/损坏。

推荐

  • 确保升级中使用的方法经过测试,否则容器变得不可变。

  • 制定可以重新安装容器的灾难恢复策略可能会很有用。

  • 请参阅“备份和恢复”部分How to audit an Internet Computer canister

容器间调用和回滚

不要在等待之后恐慌,不要跨等待边界锁定共享资源

安全问题

恐慌和陷阱回滚罐状态。因此,任何伴随陷阱或恐慌的状态变化都是值得关注的。当进行容器间调用时,这也是一个重要的问题。如果在容器间调用的“等待”之后发生恐慌/陷阱,则状态将恢复到容器间调用回调调用之前的快照(而不是在整个调用之前!)。

这可能是例如导致以下问题:

  • 如果在容器间调用之前状态更改导致状态不一致,并且在容器间调用之后出现恐慌,则会导致容器状态不一致。

  • 特别是,如果在容器间调用之前分配的资源(例如锁或内存)没有被释放,这可以例如导致罐子被永远锁定。

  • 通常,当开发人员预期的数据未持久化时,可能会出现错误。

请注意,容器间调用期间状态可能会发生变化

安全问题

消息(但不是整个调用)以原子方式处理。 这可能会导致安全问题,例如:

  • Time-of-check time-of-use:在容器间调用之前检查全局状态的某些条件,并错误地假设它在调用返回时仍然保持。

推荐

仅对可信赖的容器进行容器间调用

安全问题

  • 如果对潜在的恶意容器进行容器间调用,这可能会导致 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"

确保调用图中没有循环

安全问题

调用图中的循环(例如容器 A 调用 B、B 调用 C、C 调用 A)可能导致容器死锁。

推荐

罐升级

在升级过程中小心恐慌

安全问题

如果容器在“pre_upgrade”中陷入陷阱或恐慌,这可能会导致容器永久阻塞,从而导致升级失败或根本无法升级的情况。

推荐

  • 避免在 pre_upgrade 挂钩中出现恐慌/陷阱,除非它确实不可恢复,因此任何无效状态都可以通过升级修复。 pre-upgrade hook 中的 panic 会阻止升级,并且由于 pre-upgrade hook 由旧代码控制,它可以永久阻止升级。

  • 如果状态无效,则在 post_upgrade 挂钩中出现恐慌,以便可以重试升级并尝试修复无效状态。 升级后挂钩中的恐慌会中止升级,但可以使用新代码重试。

  • 测试升级钩子。(来自有效的rust容器)

  • 另请参阅有关升级的部分如何审核互联网计算机容器(尽管侧重于 Motoko)

  • 参见互联网计算机的当前限制,“pre_upgrade 挂钩中的错误”部分

杂项

即使存在系统 API 调用,也可以测试您的容器代码

安全问题

由于容器与系统 API 交互,因此很难测试代码,因为单元测试无法调用系统 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
        }

        (...)

    }

使容器构建可重现

安全问题

应该可以验证容器是否按照它声称的那样做。 IC 提供已部署 WASM 模块的 SHA256 哈希。 为了使其有用,容器构建必须是可重现的。

推荐

使容器构建可重现。 看到这个recommendation (from Effective Rust Canisters). See also Developer docs on this.

从您的容器中公开指标

安全问题

万一受到攻击,至少能从容器中获取相关指标是一件很好的事情,例如账户数量、内部数据结构大小、稳定内存等。

不要依赖时间是严格单调的

安全问题

从 System API 读取的时间是单调的,但不是严格单调的。因此,两个后续调用可能会返回相同的时间,这可能会在使用时间 API 时导致安全漏洞。

推荐

请参阅“时间不是严格单调的”部分如何审核 Internet 计算机容器

防止耗尽循环余额

安全问题

容器为它们的燃料费,这使得它们天生就容易受到消耗所有燃料费的攻击。

推荐

考虑监控、早期身份验证、容器级别的速率限制以减轻这种情况。另外,请注意,攻击者的目标是消耗大多数燃料费的调用。请参阅“循环平衡消耗攻击部分”如何审核 Internet 计算机容器

非特定于 Internet 计算机

本节中的最佳实践非常笼统,并不特定于 Internet 计算机。此列表绝不是完整的,仅列出了过去导致问题的一些非常具体的问题。

验证输入

安全问题

发送的数据查询和更新调用一般是不可信的。消息大小限制为几 MB。这可以例如引导以下问题:

  • 如果未经验证的数据在 Web UI 中呈现或显示在其他系统中,这可能导致注入攻击(例如 XSS)。

  • 大尺寸的消息可能会被发送并可能存储在容器中,从而消耗过多的存储空间。

  • 大输入(例如大列表或字符串)可能会触发过多的计算,导致 DoS 并消耗许多燃料费。另见防止耗尽燃料费余额

推荐

  • 执行输入验证,参见例如OWASP备忘单

  • “大数据攻击”部分如何审核 Internet 计算机容器(注意坦率的太空炸弹)

  • ASVS 5.1.4:验证结构化数据是强类型的,并针对定义的模式进行验证,包括允许的字符、长度和模式(例如信用卡号码或电话,或验证两个相关字段是否合理,例如检查郊区和邮政编码匹配)。

Rust:不要使用不安全的 Rust 代码

安全问题

不安全的 Rust 代码是有风险的,因为它可能会引入内存损坏问题。

推荐

Rust:避免整数溢出

安全问题

Rust 中的整数可能会溢出。虽然这样的溢出会导致调试配置出现恐慌,但这些值只是在发布编译中默默地包裹起来。这可能会导致重大的安全问题,例如当整数用作索引、唯一 ID 或计算燃料费期或 ICP 量时。

推荐

  • 仔细检查您的代码是否存在任何可能环绕的整数运算。

  • 使用这些操作的“饱和”或“已检查”变体,例如“饱和添加”、“饱和订阅”、“检查添加”、“检查订阅”等。参见例如Rust docs for u32

  • 另见Rust security Guidelines on integer overflows

对于昂贵的调用,考虑使用验证码或工作证明

安全问题

如果更新或查询调用很昂贵,例如就使用的内存或消耗的燃料费而言,这可能使机器人很容易使容器不可用(例如,通过填满它的存储空间)。

推荐

如果 dApp 提供此类操作,请考虑使用 bot 预防技术,例如添加验证码或工作量证明。有例如internet identity中的验证码实现。