Skip to content

Insecure deserialization in CSRF token validation via attacker-controlled biny-csrf cookie. #143

Description

@fa1c4

1. Vulnerability Topic

Insecure deserialization in CSRF token validation via attacker-controlled biny-csrf cookie.

2. Vendor / GitHub Repo

Tencent / Tencent/Biny

3. Product Name

Biny PHP Framework

4. Release Version / Commit Hash / Affected Range

Observed in the packaged source for Biny release 2.10.11 as indicated in ReadMe.md.

Affected range is not fully determined from the minimal source package. Any version containing the same Request::validateCsrfToken() logic is likely affected.

5. Vulnerability Type

PHP object injection through unsafe deserialization of attacker-controlled cookie data.

6. CWE

CWE-502: Deserialization of Untrusted Data

7. Vulnerability Summary

biny\lib\Request::validateCsrfToken() deserializes the biny-csrf cookie before verifying the HMAC that is prepended when the cookie is generated. An attacker can supply any 64-byte prefix followed by a serialized PHP payload. The code strips the prefix and passes the remaining bytes to unserialize(), enabling attacker-controlled object deserialization before the CSRF token comparison fails.

The standalone PoC demonstrates that a crafted cookie reaches unserialize() and executes a test gadget's __wakeup() method even though validateCsrfToken() ultimately returns false.

8. Root Cause

Cookie creation adds a hash prefix:

private function hashData($data, $key)
{
    $hash = hash_hmac('sha256', $data, $key);
    return $hash . $data;
}

createCsrfToken() stores:

App::$base->response->setCookie($trueKey, $this->hashData(serialize([$trueKey, $trueToken]), 'platformtest'));

But validation only calculates the expected hash length and strips that many bytes. It never verifies that the prefix matches the serialized suffix:

$trueToken = $_COOKIE[$trueToken];
$test = @hash_hmac('sha256', '', '', false);
$hashLength = mb_strlen($test, '8bit');
$trueToken = unserialize(mb_substr($trueToken, $hashLength, mb_strlen($trueToken, '8bit'), '8bit'))[1];

The missing integrity check turns a cookie value into an untrusted deserialization source.

9. Attack Preconditions

  • The target application uses Biny's default Action base class with $csrfValidate = true.
  • The attacker can cause a non-GET/HEAD/OPTIONS request to reach a Biny action from a non-whitelisted IP.
  • The attacker can set or inject the biny-csrf cookie value for the target application domain.
  • Practical high-impact exploitation requires an autoloaded PHP class with exploitable magic methods (__wakeup, __destruct, __toString, etc.).

10. Impact Analysis

The vulnerability provides a framework-level PHP object injection primitive. The PoC proves magic-method execution during CSRF validation. Depending on the application and installed dependencies, this can lead to arbitrary code execution, file write/delete, SSRF, data exfiltration, or denial of service. Even without a known gadget chain in the minimal source package, deserializing attacker-controlled cookie data before authentication/CSRF rejection is a security vulnerability.

11. Affected Code

File: lib/business/Request.php

Relevant code:

$trueToken = $_COOKIE[$trueToken];
$test = @hash_hmac('sha256', '', '', false);
$hashLength = mb_strlen($test, '8bit');
$trueToken = unserialize(mb_substr($trueToken, $hashLength, mb_strlen($trueToken, '8bit'), '8bit'))[1];

Default reachability through lib/business/Action.php:

protected $csrfValidate = true;
...
if ($this->csrfValidate && !$this->request->validateCsrfToken()) {
    header(App::$base->config->get(401, 'http'));
    echo $this->response->error("Unauthorized");
    exit;
}

12. PoC

https://github.com/fa1c4/security-advisories/tree/main/biny

Expected vulnerable output:

validateCsrfToken() returned: false
wakeup marker: TRIGGERED
[VULNERABLE] Attacker-controlled biny-csrf cookie reached unserialize() and executed ProofGadget::__wakeup() before CSRF rejection.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions