GitHub Security Advisory: GHSA-fcrh-fqxh-6fx6
Package: idno/known (COMPOSER)
Severity: CRITICAL (CVSS 0.0)
CWE: CWE-918: Server-Side Request Forgery (SSRF)
Vulnerable versions: <= 1.6.3
Patched in: 1.6.4

Summary: Idno Vulnerable to Unauthenticated SSRF via URL Unfurl Endpoint

Details:
## Summary
A logic error in the API authentication flow causes the CSRF protection on the URL unfurl service endpoint to be trivially bypassed by any unauthenticated remote attacker. Combined with the absence of a login requirement on the endpoint itself, this allows an attacker to force the server to make arbitrary outbound HTTP requests to any host, including internal network addresses and cloud instance metadata services, and retrieve the response content.

**Component**: `Idno/Pages/Service/Web/UrlUnfurl.php`, `Idno/Core/Session.php`, `Idno/Core/Actions.php`  
**Vulnerability Class**: [Server-Side Request Forgery (SSRF)](https://cwe.mitre.org/data/definitions/918.html)
**Authentication Required**: None  
**CVSSv4 Base Score:** 9.2 (High) - [AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N](https://www.first.org/cvss/calculator/4.0#CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N)
**Affected Endpoint**: GET `/service/web/unfurl?url=<attacker-controlled-url>`
Handled by `Idno\Pages\Service\Web\UrlUnfurl::getContent()`.
**Affected Versions**: <= 1.6.3
```
cat version.idno
version = '1.6.3'
build =[PII_REDACTED]
```


## Code Flow  
### Step 1 — Endpoint access control  
`UrlUnfurl::getContent()` (UrlUnfurl.php:36) enforces two access controls:  
`$this->xhrGatekeeper();`
`$this->tokenGatekeeper();`  

Notably, the original authentication check (`$this->gatekeeper()`) was explicitly removed with the following comment left in the source:

```php
//$this->gatekeeper(); // Gatekeeper to ensure this service isn't abused by third parties
// UPDATE: Needs to be accessible to logged out users, TODO, find a way to prevent abuse
```

This leaves the endpoint accessible to unauthenticated users, with only the two remaining gatekeepers as a barrier.

### Step 2 — Bypassing xhrGatekeeper()
`Page::xhrGatekeeper()` (Page.php:876) checks whether the request was made with the X-Requested-With:
XMLHttpRequest header:
```php
function xhrGatekeeper()
{
    if (!$this->xhr) {
        $this->deniedContent();
    }
}
```

This check is trivially bypassed by any HTTP client capable of setting custom headers.

### Step 3 — Bypassing `tokenGatekeeper()` via premature API flag
`Page::tokenGatekeeper()` (Page.php:887) calls `Actions::validateToken()`:
```php
function tokenGatekeeper()
{
    $url = $this->currentUrl();
    $bits = explode('?', $url);
    $url = $bits[0];
    if (!\Idno\Core\Idno::site()->actions()->validateToken($url, false)) {
        $this->deniedContent();
    }
}
```
`Actions::validateToken()` (Actions.php:23) short-circuits entirely when `isAPIRequest()` returns true:

```php
public static function validateToken($action = '', $haltExecutionOnBadRequest = true)
{
    if (Idno::site()->session()->isAPIRequest()) {
        return true;
    }
    return parent::validateToken($action, $haltExecutionOnBadRequest);
}
```
`isAPIRequest()` reads the `is_api_request` flag from the session:
```php
function isAPIRequest()
{
    if (!empty($_SESSI