VYPR
Medium severity6.5OSV Advisory· Published Dec 16, 2025· Updated Apr 15, 2026

CVE-2025-68113

CVE-2025-68113

Description

ALTCHA is privacy-first software for captcha and bot protection. A cryptographic semantic binding flaw in ALTCHA libraries allows challenge payload splicing, which may enable replay attacks. The HMAC signature does not unambiguously bind challenge parameters to the nonce, allowing an attacker to reinterpret a valid proof-of-work submission with a modified expiration value. This may allow previously solved challenges to be reused beyond their intended lifetime, depending on server-side replay handling and deployment assumptions. The vulnerability primarily impacts abuse-prevention mechanisms such as rate limiting and bot mitigation. It does not directly affect data confidentiality or integrity. This issue has been addressed by enforcing explicit semantic separation between challenge parameters and the nonce during HMAC computation. Users are advised to upgrade to patched versions, which include version 1.0.0 of the altcha Golang package, version 1.0.0 of the altcha Rubygem, version 1.0.0 of the altcha pip package, version 1.0.0 of the altcha Erlang package, version 1.4.1 of the altcha-lib npm package, version 1.3.1 of the altcha-org/altcha Composer package, and version 1.3.0 of the org.altcha:altcha Maven package. As a mitigation, implementations may append a delimiter to the end of the salt value prior to HMAC computation (for example, <salt>?expires=<time>&). This prevents ambiguity between parameters and the nonce and is backward-compatible with existing implementations, as the delimiter is treated as a standard URL parameter separator.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
altcha-libnpm
< 1.4.11.4.1
altcha-org/altchaPackagist
< 1.3.11.3.1
github.com/altcha-org/altcha-lib-goGo
< 1.0.01.0.0
org.altcha:altchaMaven
< 1.3.01.3.0
altchaRubyGems
< 1.0.01.0.0
altchaPyPI
< 1.0.01.0.0
altchaHex
< 1.0.01.0.0

Affected products

1

Patches

6
4fd7b64cbbfc

fix: salt parameter splicing

https://github.com/altcha-org/altcha-lib-rbDaniel RegeciDec 14, 2025via ghsa
2 files changed · +21 1
  • lib/altcha.rb+1 0 modified
    @@ -254,6 +254,7 @@ def self.create_challenge(options)
     
         salt = options.salt || random_bytes(salt_length).unpack1('H*')
         salt += "?#{URI.encode_www_form(params)}" unless params.empty?
    +    salt += salt.end_with?('&') ? '' : '&'
     
         number = options.number || random_int(max_number)
     
    
  • spec/altcha_spec.rb+20 1 modified
    @@ -87,6 +87,25 @@
           expect(Altcha.verify_solution(payload, hmac_key, true)).to be true
         end
     
    +    it 'fails to verify an incorrect solution with salt splicing' do
    +      challenge_options_with_expires = Altcha::ChallengeOptions.new(
    +        algorithm: algorithm,
    +        expires: Time.now.to_i + 3600,
    +        hmac_key: hmac_key,
    +        salt: salt,
    +        number: 123
    +      ) 
    +      challenge = Altcha.create_challenge(challenge_options_with_expires)
    +      payload = {
    +        algorithm: algorithm,
    +        challenge: challenge.challenge,
    +        number: 23,
    +        salt: challenge.salt + '1',
    +        signature: challenge.signature
    +      }
    +      expect(Altcha.verify_solution(payload, hmac_key, true)).to be false
    +    end
    +
         it 'fails to verify an incorrect solution' do
           payload = { algorithm: algorithm, challenge: 'wrong_challenge', number: number, salt: salt, signature: 'wrong_signature' }
           expect(Altcha.verify_solution(payload, hmac_key, false)).to be false
    @@ -131,7 +150,7 @@
       describe '.solve_challenge' do
         it 'solves a challenge correctly' do
           challenge = Altcha.create_challenge(challenge_options)
    -      solution = Altcha.solve_challenge(challenge.challenge, salt, algorithm, 10_000, 0)
    +      solution = Altcha.solve_challenge(challenge.challenge, challenge.salt, algorithm, 10_000, 0)
           expect(solution).not_to be_nil
           expect(solution.number).to eq(number)
         end
    
09b2bad466ad

fix: salt parameter splicing

https://github.com/altcha-org/altcha-lib-exDaniel RegeciDec 14, 2025via ghsa
2 files changed · +37 3
  • lib/altcha.ex+9 0 modified
    @@ -449,6 +449,7 @@ defmodule Altcha do
         params =
           if expires do
             time = if is_binary(expires), do: expires, else: Integer.to_string(expires)
    +
             Map.put(
               params,
               "expires",
    @@ -467,6 +468,14 @@ defmodule Altcha do
             salt
           end
     
    +    # Add a delimiter to prevent parameter splicing
    +    salt =
    +      if String.ends_with?(salt, "&") do
    +        salt
    +      else
    +        salt <> "&"
    +      end
    +
         number = number || random_int(max_number)
     
         challenge_str = "#{salt}#{number}"
    
  • test/altcha_test.exs+28 3 modified
    @@ -223,6 +223,30 @@ defmodule AltchaTest do
     
           refute Altcha.verify_solution(invalid_payload, @valid_hmac_key)
         end
    +
    +    test "returns false for invalid payload with salt splicing" do
    +      challenge_options = %ChallengeOptions{
    +        algorithm: :sha256,
    +        number: 123,
    +        salt_length: 16,
    +        hmac_key: @valid_hmac_key
    +      }
    +
    +      challenge = Altcha.create_challenge(challenge_options)
    +
    +      payload =
    +        %Payload{
    +          algorithm: challenge.algorithm,
    +          challenge: challenge.challenge,
    +          number: 23,
    +          salt: challenge.salt <> "1",
    +          signature: challenge.signature
    +        }
    +        |> Payload.to_json()
    +        |> Base.encode64()
    +
    +      refute Altcha.verify_solution(payload, @valid_hmac_key)
    +    end
       end
     
       describe "verify_server_signature/2" do
    @@ -231,13 +255,14 @@ defmodule AltchaTest do
             %ServerSignaturePayload{
               algorithm: "SHA-256",
               verification_data: "verified=true",
    -          signature: Altcha.hmac_hex(Altcha.hash("verified=true", :sha256), :sha256, @valid_hmac_key),
    +          signature:
    +            Altcha.hmac_hex(Altcha.hash("verified=true", :sha256), :sha256, @valid_hmac_key),
               verified: true
             }
             |> ServerSignaturePayload.to_json()
             |> Base.encode64()
     
    -      assert { true, _ } = Altcha.verify_server_signature(payload, @valid_hmac_key)
    +      assert {true, _} = Altcha.verify_server_signature(payload, @valid_hmac_key)
         end
     
         test "returns false for invalid server signature" do
    @@ -251,7 +276,7 @@ defmodule AltchaTest do
             |> Jason.encode!()
             |> Base.encode64()
     
    -      assert { false, _ } = Altcha.verify_server_signature(invalid_payload, @valid_hmac_key)
    +      assert {false, _} = Altcha.verify_server_signature(invalid_payload, @valid_hmac_key)
         end
       end
     
    
69277651fdd6

fix: salt parameter splicing

https://github.com/altcha-org/altcha-lib-javaDaniel RegeciDec 14, 2025via ghsa
2 files changed · +24 0
  • src/main/java/org/altcha/altcha/Altcha.java+5 0 modified
    @@ -376,6 +376,11 @@ public static Challenge createChallenge(ChallengeOptions options) throws Excepti
                 salt += "?" + encodeParams(params);
             }
     
    +        // Add a delimiter to prevent parameter splicing
    +        if (!salt.endsWith("&")) {
    +            salt += "&";
    +        }
    +
             long number = options.number != null ? options.number
                     : (options.secureRandomNumber ? randomIntSecure(maxNumber) : randomInt(maxNumber));
             String challengeStr = hashHex(algorithm, salt + number);
    
  • src/test/java/org/altcha/altcha/AltchaTest.java+19 0 modified
    @@ -74,6 +74,25 @@ public void testVerifySolution() throws Exception {
             assertTrue(isValid);
         }
     
    +    @Test
    +    public void testVerifySolutionSaltSplicing() throws Exception {
    +        Altcha.ChallengeOptions options = new Altcha.ChallengeOptions();
    +        options.number = 123L;
    +        options.hmacKey = "secret";
    +
    +        Altcha.Challenge challenge = Altcha.createChallenge(options);
    +
    +        Altcha.Payload payload = new Altcha.Payload();
    +        payload.algorithm = challenge.algorithm;
    +        payload.challenge = challenge.challenge;
    +        payload.number = 23L;
    +        payload.salt = challenge.salt + "1";
    +        payload.signature = challenge.signature;
    +
    +        boolean isValid = Altcha.verifySolution(payload, options.hmacKey, false);
    +        assertFalse(isValid);
    +    }
    +
         @Test
         public void testExtractParams() throws Exception {
             Map<String, String> params = Altcha.extractParams("testSalt?param1=value1&param2=value2");
    
4a5610745ef7

fix: salt parameter splicing

https://github.com/altcha-org/altcha-lib-goDaniel RegeciDec 14, 2025via ghsa
2 files changed · +39 0
  • altcha.go+5 0 modified
    @@ -386,6 +386,11 @@ func CreateChallenge(options ChallengeOptions) (Challenge, error) {
     		salt = salt + "?" + params.Encode()
     	}
     
    +	// Add a delimiter to prevent parameter splicing
    +	if !strings.HasSuffix(salt, "&") {
    +		salt += "&"
    +	}
    +
     	var number int64
     	if options.Number == nil {
     		randomNumber, err := randomInt(maxNumber)
    
  • altcha_test.go+34 0 modified
    @@ -253,6 +253,40 @@ func TestVerifySolutionSafe(t *testing.T) {
     	}
     }
     
    +func TestVerifySolutionSaltSplicing(t *testing.T) {
    +	expires := time.Now().Add(10 * time.Minute)
    +	var num int64 = 123
    +	options := ChallengeOptions{
    +		HMACKey:    "test-key",
    +		SaltLength: 16,
    +		Algorithm:  SHA256,
    +		Expires:    &expires,
    +		Number:     &num,
    +		Params:     url.Values{"foo": {"bar"}},
    +	}
    +
    +	challenge, err := CreateChallenge(options)
    +	if err != nil {
    +		t.Fatalf("CreateChallenge() error = %v", err)
    +	}
    +
    +	payload := Payload{
    +		Algorithm: challenge.Algorithm,
    +		Challenge: challenge.Challenge,
    +		Number:    23,
    +		Salt:      challenge.Salt + "1",
    +		Signature: challenge.Signature,
    +	}
    +
    +	valid, err := VerifySolution(payload, "test-key", true)
    +	if err != nil {
    +		t.Fatalf("VerifySolution() error = %v", err)
    +	}
    +	if valid {
    +		t.Error("VerifySolution() should return false for invalid spliced solution")
    +	}
    +}
    +
     func TestExtractParams(t *testing.T) {
     	payload := Payload{
     		Salt: "abc123?foo=bar&baz=qux",
    
9e9e70c864a9

fix: replace ; with & in salt delimiter for compatibility

https://github.com/altcha-org/altcha-lib-phpDaniel RegeciDec 13, 2025via ghsa
2 files changed · +3 3
  • src/Altcha.php+1 1 modified
    @@ -154,7 +154,7 @@ public function verifySolution(string|array $data, bool $checkExpires = true): b
          */
         private function extractParams(Payload $payload): array
         {
    -        $saltParts = explode('?', rtrim($payload->salt, ';'));
    +        $saltParts = explode('?', $payload->salt);
             if (\count($saltParts) > 1) {
                 parse_str($saltParts[1], $params);
     
    
  • src/BaseChallengeOptions.php+2 2 modified
    @@ -39,8 +39,8 @@ public function __construct(
             }
     
             // Add a delimiter to prevent parameter splicing
    -        if (!str_ends_with($salt, ';')) {
    -            $salt .= ';';
    +        if (!str_ends_with($salt, '&')) {
    +            $salt .= '&';
             }
     
             $this->salt = $salt;
    
cb95d83a8d08

fix: replace ; with & in salt delimiter for compatibility

https://github.com/altcha-org/altcha-libDaniel RegeciDec 13, 2025via ghsa
5 files changed · +13 13
  • cjs/dist/index.js+3 3 modified
    @@ -31,8 +31,8 @@ async function createChallenge(options) {
             salt = salt + '?' + params.toString();
         }
         // Add a delimiter to prevent parameter splicing
    -    if (!salt.endsWith(';')) {
    -        salt = salt + ';';
    +    if (!salt.endsWith('&')) {
    +        salt = salt + '&';
         }
         const number = options.number === undefined ? (0, helpers_js_1.randomInt)(maxnumber) : options.number;
         const challenge = await (0, helpers_js_1.hashHex)(algorithm, salt + number);
    @@ -54,7 +54,7 @@ function extractParams(payload) {
         if (typeof payload === 'string') {
             payload = JSON.parse(atob(payload));
         }
    -    return Object.fromEntries(new URLSearchParams(payload?.salt?.split('?')?.[1]?.replace(/;$/, '') || ''));
    +    return Object.fromEntries(new URLSearchParams(payload?.salt?.split('?')?.[1] || ''));
     }
     /**
      * Verifies the solution provided by the client.
    
  • deno_dist/index.ts+3 3 modified
    @@ -43,8 +43,8 @@ export async function createChallenge(
         salt = salt + '?' + params.toString();
       }
       // Add a delimiter to prevent parameter splicing
    -  if (!salt.endsWith(';')) {
    -    salt = salt + ';';
    +  if (!salt.endsWith('&')) {
    +    salt = salt + '&';
       }
       const number =
         options.number === undefined ? randomInt(maxnumber) : options.number;
    @@ -69,7 +69,7 @@ export function extractParams(payload: string | Payload | Challenge) {
         payload = JSON.parse(atob(payload)) as Payload;
       }
       return Object.fromEntries(
    -    new URLSearchParams(payload?.salt?.split('?')?.[1]?.replace(/;$/, '') || '')
    +    new URLSearchParams(payload?.salt?.split('?')?.[1] || '')
       );
     }
     
    
  • dist/index.js+3 3 modified
    @@ -22,8 +22,8 @@ export async function createChallenge(options) {
             salt = salt + '?' + params.toString();
         }
         // Add a delimiter to prevent parameter splicing
    -    if (!salt.endsWith(';')) {
    -        salt = salt + ';';
    +    if (!salt.endsWith('&')) {
    +        salt = salt + '&';
         }
         const number = options.number === undefined ? randomInt(maxnumber) : options.number;
         const challenge = await hashHex(algorithm, salt + number);
    @@ -45,7 +45,7 @@ export function extractParams(payload) {
         if (typeof payload === 'string') {
             payload = JSON.parse(atob(payload));
         }
    -    return Object.fromEntries(new URLSearchParams(payload?.salt?.split('?')?.[1]?.replace(/;$/, '') || ''));
    +    return Object.fromEntries(new URLSearchParams(payload?.salt?.split('?')?.[1] || ''));
     }
     /**
      * Verifies the solution provided by the client.
    
  • lib/index.ts+3 3 modified
    @@ -43,8 +43,8 @@ export async function createChallenge(
         salt = salt + '?' + params.toString();
       }
       // Add a delimiter to prevent parameter splicing
    -  if (!salt.endsWith(';')) {
    -    salt = salt + ';';
    +  if (!salt.endsWith('&')) {
    +    salt = salt + '&';
       }
       const number =
         options.number === undefined ? randomInt(maxnumber) : options.number;
    @@ -69,7 +69,7 @@ export function extractParams(payload: string | Payload | Challenge) {
         payload = JSON.parse(atob(payload)) as Payload;
       }
       return Object.fromEntries(
    -    new URLSearchParams(payload?.salt?.split('?')?.[1]?.replace(/;$/, '') || '')
    +    new URLSearchParams(payload?.salt?.split('?')?.[1] || '')
       );
     }
     
    
  • tests/challenge.test.ts+1 1 modified
    @@ -106,7 +106,7 @@ describe('challenge', () => {
             signature: expect.any(String),
           } satisfies Challenge);
           expect(challenge.salt.length).toBeGreaterThan(25);
    -      expect(challenge.salt.endsWith('?abc=123&xyz=000;')).toBeTruthy();
    +      expect(challenge.salt.endsWith('?abc=123&xyz=000&')).toBeTruthy();
           expect(challenge.challenge.length).toEqual(64);
           expect(challenge.signature.length).toEqual(64);
         });
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

13

News mentions

0

No linked articles in our index yet.