Skip to content

Light mode skips signature verification #308

Description

@Carrotrust

validation.js's top-level validate() function, when conf.bLight is true, returns success for any joint without a ball field before ever reaching validateAuthors() / Definition.validateAuthentifiers() — i.e. before any cryptographic signature is checked at all:

// validation.js:340-348
if (conf.bLight){
    if (!isPositiveInteger(objUnit.timestamp) && !objJoint.unsigned)
        return callbacks.ifJointError("bad timestamp");
    if (objJoint.ball)
        return callbacks.ifJointError(lightStableErrorMessage);
    return objJoint.unsigned 
        ? callbacks.ifOkUnsigned(true) 
        : callbacks.ifOk({sequence: 'good', arrDoubleSpendInputs: [], arrAdditionalQueries: []}, function(){});
}

validateLight() (validation.js:97-116) is a thin synchronous wrapper around this same validate(), and is exactly what light.js's processHistory calls on every joint in a hub's light/get_history response (light.js:245). Two checks that would have caught this were deliberately commented out (light.js:241-244, 251-252, commit 4975d23) with the justification "we receive unconfirmed units too" — a legitimate reason to accept genuinely unconfirmed/pending units, but that only justifies skipping the stability/proofchain check, not skipping signature verification entirely, which appears to be an unintended side effect rather than a considered decision.

Impact: a malicious or compromised light-vendor hub can inject a fully fabricated joint (arbitrary author address, garbage/empty authentifiers, self-consistent attacker-computed hash) into a get_history response, with a payment input referencing a victim's real, currently-unspent output. writer.saveJoint accepts it (since validateLight returned success), and light.js's fixIsSpentFlag (light.js:448-469) then flips the victim's real output to is_spent=1 in the local wallet database, based on nothing but this unverified, forged inputs row — with zero cryptographic basis. The victim's wallet will refuse to spend that real, intact output for as long as they stay connected to the malicious hub (the light-vendor URL is sticky and doesn't auto-fallback, and the 24h self-correction path queries the same hub that lied in the first place).

Note on real-world impact: the underlying value is never destroyed or moved to the attacker — it's a false-spent flag local to one victim's light-client database, recoverable by switching light vendor or re-importing the same seed elsewhere. This is why it doesn't meet a fund-loss bar, but it's still a real breach of the light-client protocol's core trust model (a light client is supposed to be safe against a lying hub for exactly this kind of thing).

Suggested fix: for unstable (non-ball) joints in light mode, still require and verify validateAuthors()/signature checks against the unit's declared author definitions, rather than skipping straight to ifOk.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions