OpenZeppelin / OpenZeppelin/stellar-contracts

`weighted_threshold::install` does not validate that weighted addresses are rule signers, allowing an unreachable threshold to pass install-time validation

Open
#892 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
95
Forks
68
Avg merge
4d 42m
Merged PRs (30d)
20

Description

Version: v0.7.2 · Package: packages/accounts

Summary

weighted_threshold::install validates threshold <= sum(signer_weights), but never checks that each Signer key in signer_weights belongs to the context_rule's signer set. Because enforce only counts the weights of signers that actually authenticate through the rule, weights assigned to non-rule signers inflate the install-time total while contributing nothing at enforcement. A threshold that passes install can therefore be permanently unreachable, silently bricking the context rule from construction.

Affected code (packages/accounts/src/policies/weighted_threshold.rs, v0.7.2)

install accepts the caller's signer_weights map and validates the threshold against the sum of all its values:

let total_weight = calculate_total_weight(e, &params.signer_weights);
if params.threshold == 0 || params.threshold > total_weight {
    panic_with_error!(e, WeightedThresholdError::InvalidThreshold);
}

calculate_total_weight sums every value in the map, with no reference to context_rule.signers:

fn calculate_total_weight(e: &Env, signer_weights: &Map<Signer, u32>) -> u32 {
    let mut total_weight: u32 = 0;
    for weight in signer_weights.values() { total_weight = total_weight.checked_add(weight)... }
    total_weight
}

At enforcement, calculate_weight only credits weights for signers passed in authenticated_signers (constrained to the rule's signers by do_check_auth):

for signer in signers.iter() {
    if let Some(weight) = signer_weights.get(signer.clone()) { total_weight = total_weight.checked_add(weight)... }
}

install receives the fully-populated context_rule (see smart_account/storage.rs::add_context_rule, which builds context_rule with signers before calling PolicyClient::install(&param, &context_rule, ...)), so the signer set needed to validate is already in scope — it is simply not consulted.

Impact

  • Configuration-time lockout: a rule can be created whose weighted-threshold policy can never be satisfied. Availability, not fund loss; it requires the account owner to misconfigure at setup, but the current InvalidThreshold check gives false assurance that a passing install is reachable.
  • Reachable via the public constructor / add_context_rule path.

Reproduction (concept)

  1. Create a context rule with signers {A, B}.
  2. Install weighted_threshold with signer_weights = {A: 1, B: 1, X: 10} where X is not a rule signer, threshold = 11.
  3. install passes: sum = 12 >= 11.
  4. enforce with any subset of {A, B} maxes at weight 2 < 11NotAllowed. The rule is permanently unusable.

Suggested fix

In install (and any setter that modifies signer_weights), reject entries whose Signer key is not present in context_rule.signers, or compute total_weight only over rule signers. The context_rule is already passed in, so the guard is local:

for signer in params.signer_weights.keys() {
    if !context_rule.signers.contains(&signer) {
        panic_with_error!(e, WeightedThresholdError::InvalidThreshold); // or a dedicated error
    }
}

Notes

Reported as a hardening / configuration-safety gap, not a library exploit. Found while building RuleLens, an open-source configuration checker for deployed OZ Stellar smart accounts (https://github.com/bilhokista/rulelens). Happy to open a PR with the guard + a regression test if the maintainers agree the check belongs at the policy level.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/accounts/src/policies/weighted_threshold.rs and trace install alongside calculate_total_weight and calculate_weight. Read smart_account/storage.rs::add_context_rule to confirm how context_rule.signers reaches PolicyClient::install. Done means non-rule signers cannot make an unreachable threshold pass installation, with a regression test covering the A/B/X scenario.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
authorization, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.