Make stratum implementation align with ckpool #560

Open
opened 2026-05-27 14:06:10 +00:00 by paratoxic · 1 comment
Owner

para too strict

  1. Nonce2 Length (MAJOR)ckpool: Silently truncates or pads nonce2 to correct lengthif (nlen > len) nonce2[len] = '\0'; // Truncate
    else if (nlen < len) { /* Pad with zeros */ }
    para: Strictly rejects with InvalidNonce2Length error
    if submit.enonce2.len() != expected_extranonce2_size {
    // Send error and reject share
    }
  2. Nonce Length
    ckpool: Silently truncates nonce to 8 characters
    if (unlikely(nlen > len)) nonce[len] = '\0'; // len = 8
    para: Strict hex parsing via Nonce::from_str
  3. Version Mask (mining.submit)
    ckpool: Ignores invalid version_mask, uses 0
    if (version_mask && strlen(version_mask) && validhex(version_mask)) {
    // Only parse if valid
    }
    // Otherwise version_mask32 stays 0, no error
    para: Strict deserialization fails on invalid version
  4. Empty Submit Params
    ckpool: Accepts missing password (sets to "")
    para: Also accepts (via Option)

para too lenient

  1. Nonce Minimum Length (CRITICAL)
    ckpool: Requires nonce to be at least 8 hex characters
    if (unlikely(!nonce || strlen(nonce) < 8 || !validhex(nonce))) {
    err = SE_NO_NONCE; // Error if < 8 chars
    }
    para: Accepts any valid u32 hex (as short as "0")
    let nonce = u32::from_str_radix(s, 16)?; // "0", "1", "abc" all valid
  2. Odd-Length Hex Strings
    ckpool: Strictly rejects odd-length hex
    if (!slen || slen % 2) // Rejects odd length
    para: Some hex fields accept odd-length (we just fixed subscribe, but nonce/ntime/version still accept odd-length via from_str_radix)
  3. Username Validation
    ckpool: Rejects:
  • Empty usernames
  • Usernames starting with "." or "_"
  • Usernames containing "/"
    para: Accepts any string, only validates as Bitcoin address later
  1. Password Length
    ckpool: Max 64 characters (strndup(pass, 64))
    para: No explicit limit
  2. Empty Nonce
    ckpool: Rejects empty nonce strings
    para: Would fail parsing but error message differs

Misc

Nonce2/Nonce Length Tolerance

ckpool auto-corrects malformed nonce2 and nonce values from miners:

  • nonce2: If the miner sends fewer hex chars than expected
    (wb->enonce2varlen), ckpool truncates silently. If longer, it also
    truncates.
  • nonce: If longer than 8 hex chars, ckpool truncates to 8 chars:
    nonce[len] = '\0' (stratifier.c:6059).

para rejects immediately with InvalidNonce2Length if enonce2.len() != expected_extranonce2_size (stratifier.rs:692). Nonce is deserialized with
a fixed 4-byte type so invalid lengths fail at parse time.

Some miner firmware (especially older or buggy implementations) may send
slightly wrong lengths. ckpool silently handles this; para rejects the
share.

### para too strict 1. Nonce2 Length (MAJOR)ckpool: Silently truncates or pads nonce2 to correct lengthif (nlen > len) nonce2[len] = '\0'; // Truncate else if (nlen < len) { /* Pad with zeros */ } para: Strictly rejects with InvalidNonce2Length error if submit.enonce2.len() != expected_extranonce2_size { // Send error and reject share } 2. Nonce Length ckpool: Silently truncates nonce to 8 characters if (unlikely(nlen > len)) nonce[len] = '\0'; // len = 8 para: Strict hex parsing via Nonce::from_str 3. Version Mask (mining.submit) ckpool: Ignores invalid version_mask, uses 0 if (version_mask && strlen(version_mask) && validhex(version_mask)) { // Only parse if valid } // Otherwise version_mask32 stays 0, no error para: Strict deserialization fails on invalid version 4. Empty Submit Params ckpool: Accepts missing password (sets to "") para: Also accepts (via Option<String>) ### para too lenient 1. Nonce Minimum Length (CRITICAL) ckpool: Requires nonce to be at least 8 hex characters if (unlikely(!nonce || strlen(nonce) < 8 || !validhex(nonce))) { err = SE_NO_NONCE; // Error if < 8 chars } para: Accepts any valid u32 hex (as short as "0") let nonce = u32::from_str_radix(s, 16)?; // "0", "1", "abc" all valid 2. Odd-Length Hex Strings ckpool: Strictly rejects odd-length hex if (!slen || slen % 2) // Rejects odd length para: Some hex fields accept odd-length (we just fixed subscribe, but nonce/ntime/version still accept odd-length via from_str_radix) 3. Username Validation ckpool: Rejects: - Empty usernames - Usernames starting with "." or "_" - Usernames containing "/" para: Accepts any string, only validates as Bitcoin address later 4. Password Length ckpool: Max 64 characters (strndup(pass, 64)) para: No explicit limit 5. Empty Nonce ckpool: Rejects empty nonce strings para: Would fail parsing but error message differs ### Misc Nonce2/Nonce Length Tolerance ckpool auto-corrects malformed nonce2 and nonce values from miners: - **nonce2**: If the miner sends fewer hex chars than expected (`wb->enonce2varlen`), ckpool truncates silently. If longer, it also truncates. - **nonce**: If longer than 8 hex chars, ckpool truncates to 8 chars: `nonce[len] = '\0'` (stratifier.c:6059). para rejects immediately with `InvalidNonce2Length` if `enonce2.len() != expected_extranonce2_size` (stratifier.rs:692). Nonce is deserialized with a fixed 4-byte type so invalid lengths fail at parse time. Some miner firmware (especially older or buggy implementations) may send slightly wrong lengths. ckpool silently handles this; para rejects the share.
Author
Owner

What ckpool does vs. what para now does
Concern ckpool para (this branch) Verdict
Workername length username[128] buffer; truncates at 127 Rejects >127 chars Ours stricter, deliberate (rejection beats silent rewriting)
Workername charset No filtering, bounded only Rejects control chars (ESC/DEL/newline/…) Ours stricter — this was the actual vuln
Password Bounded: strndup(pass, 64) Option, unbounded Different, not worse — see below
user_agent Stored raw (strdup), served back in admin JSON unfiltered, printed in logs Validated at Username boundary only if it were a username; user_agent itself unvalidated Below
Failed-auth backoff failed_authtime/auth_backoff throttling None Below
The two real candidate gaps — and why I recommend not fixing either

  1. Password is unbounded (vs. ckpool's 64). Divergence, yes — but look at the data flow: para's authorize() ignores authorize.password entirely (never read after deserialization; stratifier.rs:603-680). An attacker sending a 1 GB password is just an oversized-JSON-line problem, and that class of issue lives at the message/framing layer (LinesCodec/FramedRead max line length), not the username type. Bounding it at the Authorize type would be cargo-culting ckpool's buffer limit with no actual buffer to protect. Skip.
  2. No failed-auth backoff (ckpool throttles repeat bad authorize attempts). ckpool needs this because it has open-ended username → DB user creation. Para's authorize already hard-rejects on parse_with_network failure and — critically — calls self.bouncer.reject(), so the bouncer's existing consecutive-reject / drop logic (stratifier.rs:255-372) already rate-limits bad actors at the connection level. Adding ckpool-style per-user backoff would be defense-in-depth against a vector that's already bounded by connection drops. Skip — the bouncer is the para equivalent.
    The one thing I'd flag as worth a look (but not necessarily a fix)
    user_agent is the last unvalidated miner-supplied string. In ckpool it's stored raw and echoed into the admin clientlist JSON and logs — same class of issue as the workername was. In para it's captured at stratifier.rs:574 into Subscription.user_agent and… let me be precise: I traced it to state.rs but did not confirm whether it reaches logs or an HTTP response. If it stays purely in-memory and never hits a log line or dashboard, it's a non-issue (a string sitting in a struct can't hurt anyone). If it does surface anywhere rendered, it's the same ANSI-injection shape we just closed.
    That determination is a two-minute read — rg "user_agent" src/api templates/. Want me to run it and report back before you decide? My recommendation: only act if it actually escapes to a log or response; otherwise leave it.
    Everything else in the crate — the Authorize serde (untagged 1-or-2-arity), Submit.username re-validation, worker-mismatch compare — already routes through the hardened Username, so it's covered by this branch.
What ckpool does vs. what para now does Concern ckpool para (this branch) Verdict Workername length username[128] buffer; truncates at 127 Rejects >127 chars Ours stricter, deliberate (rejection beats silent rewriting) Workername charset No filtering, bounded only Rejects control chars (ESC/DEL/newline/…) Ours stricter — this was the actual vuln Password Bounded: strndup(pass, 64) Option<String>, unbounded Different, not worse — see below user_agent Stored raw (strdup), served back in admin JSON unfiltered, printed in logs Validated at Username boundary only if it were a username; user_agent itself unvalidated Below Failed-auth backoff failed_authtime/auth_backoff throttling None Below The two real candidate gaps — and why I recommend not fixing either 1. Password is unbounded (vs. ckpool's 64). Divergence, yes — but look at the data flow: para's authorize() ignores authorize.password entirely (never read after deserialization; stratifier.rs:603-680). An attacker sending a 1 GB password is just an oversized-JSON-line problem, and that class of issue lives at the message/framing layer (LinesCodec/FramedRead max line length), not the username type. Bounding it at the Authorize type would be cargo-culting ckpool's buffer limit with no actual buffer to protect. Skip. 2. No failed-auth backoff (ckpool throttles repeat bad authorize attempts). ckpool needs this because it has open-ended username → DB user creation. Para's authorize already hard-rejects on parse_with_network failure and — critically — calls self.bouncer.reject(), so the bouncer's existing consecutive-reject / drop logic (stratifier.rs:255-372) already rate-limits bad actors at the connection level. Adding ckpool-style per-user backoff would be defense-in-depth against a vector that's already bounded by connection drops. Skip — the bouncer is the para equivalent. The one thing I'd flag as worth a look (but not necessarily a fix) user_agent is the last unvalidated miner-supplied string. In ckpool it's stored raw and echoed into the admin clientlist JSON and logs — same class of issue as the workername was. In para it's captured at stratifier.rs:574 into Subscription.user_agent and… let me be precise: I traced it to state.rs but did not confirm whether it reaches logs or an HTTP response. If it stays purely in-memory and never hits a log line or dashboard, it's a non-issue (a string sitting in a struct can't hurt anyone). If it does surface anywhere rendered, it's the same ANSI-injection shape we just closed. That determination is a two-minute read — rg "user_agent" src/api templates/. Want me to run it and report back before you decide? My recommendation: only act if it actually escapes to a log or response; otherwise leave it. Everything else in the crate — the Authorize serde (untagged 1-or-2-arity), Submit.username re-validation, worker-mismatch compare — already routes through the hardened Username, so it's covered by this branch.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
parasitepool/para#560
No description provided.