CVE-2025-25194: Activitypub federation Server-Side Request Forgery (SSRF) Vulnerability

MODERATE
Advisory Identifier: GHSA-7723-35v7-qcxw
CVSS 4.0
CWE-918
Published
Advisory Summary

CVE-2025-25194 is a MODERATE (CVSS 4.0) server-side request forgery (ssrf) vulnerability (CWE-918: Server-Side Request Forgery (SSRF)) affecting Activitypub federation libraries in the crates.io ecosystem. The issue involves server-side request forgery (ssrf) in activitypub_federation. Remediation is available in the latest vendor maintenance releases.

CVE-2025-25194 at a Glance

CVE Identifier CVE-2025-25194
GHSA Identifier GHSA-7723-35v7-qcxw
Severity MODERATE
CVSS v3.1 Base Score 4.0
CWE Classification CWE-918: Server-Side Request Forgery (SSRF)
Affected Product Activitypub federation
Package Ecosystem crates.io
Published Date
Last Updated

Security Impact

CVSS: 4.0

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:N/A:N).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality Low (C:L)
Integrity None (I:N)
Availability None (A:N)

How to Fix CVE-2025-25194

To remediate this vulnerability, upgrade all affected Activitypub federation dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

activitypub_federation fixed: Latest secure release
Upgrade activitypub_federation to Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for activitypub_federation

How to Check if You Are Affected

To determine whether your application is affected by CVE-2025-25194, inspect your project dependencies for Activitypub federation:

inspect dependencies for activitypub_federation

Analyze the terminal output to locate any version of Activitypub federation in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than the patched release), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & RFC 5280 Specification

Under RFC 5280 specifications for X.509 certificates, the Name Constraints extension defines a namespace within which all subsequent subject names in a certification path must be located. Constraints apply to various name forms including directory names, IP addresses, rfc822Name (email addresses), and uniformResourceIdentifier (URI) entries. Specifically, during certificate path validation, affected versions of Activitypub federation failed to properly normalize or enforce boundary checks when domain names contained trailing dot (.) characters. Because domain names with a trailing dot signify fully qualified domain names (FQDNs), this omission allowed non-conforming certificates to be improperly validated as compliant with the defined name constraints.

Source Advisory Statement

### Summary This vulnerability allows a user to bypass any predefined hardcoded URL path or security anti-Localhost mechanism and perform an arbitrary GET request to any Host, Port and URL using a Webfinger Request. ### Details The Webfinger endpoint takes a remote domain for checking accounts as a feature, however, as per the ActivityPub spec (https://www.w3.org/TR/activitypub/#security-considerations), on the security considerations section at B.3, access to Localhost services should be prevented while running in production. The library attempts to prevent Localhost access using the following mechanism (/src/config.rs): ```rust pub(crate) async fn verify_url_valid(&self, url: &Url) -> Result<(), Error> { match url.scheme() { "https" => {} "http" => { if !self.allow_http_urls { return Err(Error::UrlVerificationError( "Http urls are only allowed in debug mode", )); } } _ => return Err(Error::UrlVerificationError("Invalid url scheme")), }; // Urls which use our local domain are not a security risk, no further verification needed if self.is_local_url(url) { return Ok(()); } if url.domain().is_none() { return Err(Error::UrlVerificationError("Url must have a domain")); } if url.domain() == Some("localhost") && !self.debug { return Err(Error::UrlVerificationError( "Localhost is only allowed in debug mode", )); } self.url_verifier.verify(url).await?; Ok(()) } ``` There are multiple issues with the current anti-Localhost implementation: 1. It does not resolve the domain address supplied by the user. 2. The Localhost check is using only a simple comparison method while ignoring more complex malicious tampering attempts. 3. It filters only localhost domains, without any regard for alternative local IP domains or other sensitive domains, such internal network or cloud metadata domains. We can reach the verify_url_valid function while sending a Webfinger request to lookup a user’s account (/src/fetch/webfinger.rs): ```rust pub async fn webfinger_resolve_actor<T: Clone, Kind>( identifier: &str, data: &Data<T>, ) -> Result<Kind, <Kind as Object>::Error> where Kind: Object + Actor + Send + 'static + Object<DataType = T>, for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>, <Kind as Object>::Error: From<crate::error::Error> + Send + Sync + Display, { let (_, domain) = identifier .splitn(2, '@') .collect_tuple() .ok_or(WebFingerError::WrongFormat.into_crate_error())?; let protocol = if data.config.debug { "http" } else { "https" }; let fetch_url = format!("{protocol}://{domain}/.well-known/webfinger?resource=acct:{identifier}"); debug!("Fetching webfinger url: {}", &fetch_url); let res: Webfinger = fetch_object_http_with_accept( &Url::parse(&fetch_url).map_err(Error::UrlParse)?, data, &WEBFINGER_CONTENT_TYPE, ) .await? .object; debug_assert_eq!(res.subject, format!("acct:{identifier}")); let links: Vec<Url> = res .links .iter() .filter(|link| { if let Some(type_) = &link.kind { type_.starts_with("application/") } else { false } }) .filter_map(|l| l.href.clone()) .collect(); for l in links { let object = ObjectId::<Kind>::from(l).dereference(data).await; match object { Ok(obj) => return Ok(obj), Err(error) => debug!(%error, "Failed to dereference link"), } } Err(WebFingerError::NoValidLink.into_crate_error().into()) } ``` The Webfinger logic takes the user account from the GET parameter “resource” and sinks the domain directly into the hardcoded Webfinger URL (“{protocol}://{domain}/.well-known/webfinger?resource=acct:{identifier}”) without any additional checks. Afterwards the user domain input will pass into the “fetch_object_http_with_accept” function and finally into the security check on “verify_url_valid” function, again, without any form of sanitizing or input validation. An adversary can cause unwanted behaviours using multiple techniques: 1. **_Gaining control over the query’s path:_** An adversary can manipulate the Webfinger hard-coded URL, gaining full control over the GET request domain, path and port by submitting malicious input like: hacker@hacker_host:1337/hacker_path?hacker_param#, which in turn will result in the following string: http[s]://hacker_host:1337/hacker_path?hacker_param#/.well-known/webfinger?resource=acct:{identifier}, directing the URL into another domain and path without any issues as the hash character renders the rest of the URL path unrecognized by the webserver. 2. **_Bypassing the domain’s restriction using DNS resolving mechanism:_** An adversary can manipulate the security check and force it to look for internal services regardless the Localhost check by using a domain name that resolves into a local IP (such as: localh.st, for example), as the security check does not verify the resolved IP at all - any service under the Localhost domain can be reached. 3. _**Bypassing the domain’s restriction using official Fully Qualified Domain Names (FQDNs):**_ In the official DNS specifications, a fully qualified domain name actually should end with a dot. While most of the time a domain name is presented without any trailing dot, the resolver will assume it exists, however - it is still possible to use a domain name with a trailing dot which will resolve correctly. As the Localhost check is mainly a simple comparison check - if we register a “hacker@localhost.” domain it will pass the test as “localhost” is not equal to “localhost.”, however the domain will be valid (Using this mechanism it is also possible to bypass any domain blocklist mechanism). ### PoC 1. Activate a local HTTP server listening to port 1234 with a “secret.txt” file: `python3 -m http.server 1234` 2. Open the “main.rs” file inside the “example” folder on the activitypub-federated-rust project, and modify the “beta@localhost” string into “hacker@localh.st:1234/secret.txt?something=1#”. 3. Run the example using the following command: `cargo run --example local_federation axum` 4. View the console of the Python’s HTTP server and see that a request for a “secret.txt” file was performed. This proves that we can redirect the URL to any domain and path we choose. Now on the next steps we will prove that the security checks of Localhost and blocked domains can be easily bypassed (both checks use the same comparison mechanism). 1. Now open the “instance.rs” file inside the “example” folder and view that the domain “malicious.com” is blocked (you can switch it to any desired domain address). 2. Change the same “beta@localhost” string into “hacker@malicious.com” and run the example command to see that the malicious domain blocking mechanism is working as expected. 3. Now change the “hacker@malicious.com” string into “hacker@malicious.com.” string and re-initiate the example, view now that the check passed successfully. 4. You can combine both methods on “localhost.” domain (or any other domain) to verify that the FQDNs resolving is indeed successful. ### Impact Due to this issue, any user can cause the server to send GET requests with controlled path and port in an attempt to query services running on the instance’s host, and attempt to execute a Blind-SSRF gadget in hope of targeting a known vulnerable local service running on the victim’s machine. ### Fix Suggestion Modify the domain validation mechanism and implement the following checks: 1. Resolve the domain and validate it is not using any invalid IP address (internal, or cloud metadata IPs) using regexes of both IPv4 and IPv6 addresses. For Implementation example of a good SSRF prevention practice you can review a similiar project such as “Fedify” (https://github.com/dahlia/fedify/blob/main/src/runtime/url.ts) which handles external URL resource correctly. Note that it is still needed to remove unwanted characters from the URL. 2. Filter the user’s input for any unwanted characters that should not be present on a domain name, such as #,?,/, etc. 3. Perform checks that make sure the desired request path is the executed path with the same port. 4. Disable automatic HTTP redirect follows on the implemented client, as redirects can be used for security mechanisms circumvention.

CVE-2025-25194 Affected Activitypub federation Versions

1 component(s)
Product / Package Ecosystem Affected Versions Fixed Version
activitypub_federation
crates.io
≤ 0.6.2

References

Frequently Asked Questions

What is CVE-2025-25194?
CVE-2025-25194 is a security vulnerability in Activitypub federation involving Server-Side Request Forgery (SSRF) in activitypub_federation under CWE-918: Server-Side Request Forgery (SSRF).
Which Activitypub federation versions are affected?
Affected versions include releases prior to current release patches across affected packages: activitypub_federation.
What version fixes CVE-2025-25194?
Upgrading to current release patches (or the latest patched release for your respective branch) resolves the vulnerability.
Is activitypub_federation affected?
Yes, activitypub_federation is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What is the CVSS score of CVE-2025-25194?
CVE-2025-25194 has a CVSS v3.1 base score of 4.0 (MODERATE) with vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:N/A:N.
What CWE is associated with CVE-2025-25194?
CVE-2025-25194 is classified under CWE-918: Server-Side Request Forgery (SSRF) (The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.).
How can I check crates.io dependencies for CVE-2025-25194?
Run the dependency tree command: 'inspect dependencies for activitypub_federation'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-7723-35v7-qcxw",
  "modified" : "2025-02-11T00:33:48Z",
  "published" : "2025-02-10T20:25:37Z",
  "aliases" : [ "CVE-2025-25194" ],
  "summary" : "Server-Side Request Forgery (SSRF) in activitypub_federation",
  "details" : "### Summary\nThis vulnerability allows a user to bypass any predefined hardcoded URL path or security anti-Localhost mechanism and perform an arbitrary GET request to any Host, Port and URL using a Webfinger Request.\n\n### Details\nThe Webfinger endpoint takes a remote domain for checking accounts as a feature, however, as per the ActivityPub spec (https://www.w3.org/TR/activitypub/#security-considerations), on the security considerations section at B.3, access to Localhost services should be prevented while running in production.\nThe library attempts to prevent Localhost access using the following mechanism (/src/config.rs):\n```rust\npub(crate) async fn verify_url_valid(&self, url: &Url) -> Result<(), Error> {\n        match url.scheme() {\n            \"https\" => {}\n            \"http\" => {\n                if !self.allow_http_urls {\n                    return Err(Error::UrlVerificationError(\n                        \"Http urls are only allowed in debug mode\",\n                    ));\n                }\n            }\n            _ => return Err(Error::UrlVerificationError(\"Invalid url scheme\")),\n        };\n\n        // Urls which use our local domain are not a security risk, no further verification needed\n        if self.is_local_url(url) {\n            return Ok(());\n        }\n\n        if url.domain().is_none() {\n            return Err(Error::UrlVerificationError(\"Url must have a domain\"));\n        }\n\n        if url.domain() == Some(\"localhost\") && !self.debug {\n            return Err(Error::UrlVerificationError(\n                \"Localhost is only allowed in debug mode\",\n            ));\n        }\n\n        self.url_verifier.verify(url).await?;\n\n        Ok(())\n    }\n```\nThere are multiple issues with the current anti-Localhost implementation: \n\n1. It does not resolve the domain address supplied by the user.\n2. The Localhost check is using only a simple comparison method while ignoring more complex malicious tampering attempts.\n3. It filters only localhost domains, without any regard for alternative local IP domains or other sensitive domains, such internal network or cloud metadata domains.\n\nWe can reach the verify_url_valid function while sending a Webfinger request to lookup a user’s account (/src/fetch/webfinger.rs):\n\n```rust\npub async fn webfinger_resolve_actor<T: Clone, Kind>(\n    identifier: &str,\n    data: &Data<T>,\n) -> Result<Kind, <Kind as Object>::Error>\nwhere\n    Kind: Object + Actor + Send + 'static + Object<DataType = T>,\n    for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>,\n    <Kind as Object>::Error: From<crate::error::Error> + Send + Sync + Display,\n{\n    let (_, domain) = identifier\n        .splitn(2, '@')\n        .collect_tuple()\n        .ok_or(WebFingerError::WrongFormat.into_crate_error())?;\n    let protocol = if data.config.debug { \"http\" } else { \"https\" };\n    let fetch_url =\n        format!(\"{protocol}://{domain}/.well-known/webfinger?resource=acct:{identifier}\");\n    debug!(\"Fetching webfinger url: {}\", &fetch_url);\n\n    let res: Webfinger = fetch_object_http_with_accept(\n        &Url::parse(&fetch_url).map_err(Error::UrlParse)?,\n        data,\n        &WEBFINGER_CONTENT_TYPE,\n    )\n    .await?\n    .object;\n\n    debug_assert_eq!(res.subject, format!(\"acct:{identifier}\"));\n    let links: Vec<Url> = res\n        .links\n        .iter()\n        .filter(|link| {\n            if let Some(type_) = &link.kind {\n                type_.starts_with(\"application/\")\n            } else {\n                false\n            }\n        })\n        .filter_map(|l| l.href.clone())\n        .collect();\n\n    for l in links {\n        let object = ObjectId::<Kind>::from(l).dereference(data).await;\n        match object {\n            Ok(obj) => return Ok(obj),\n            Err(error) => debug!(%error, \"Failed to dereference link\"),\n        }\n    }\n    Err(WebFingerError::NoValidLink.into_crate_error().into())\n}\n```\n\nThe Webfinger logic takes the user account from the GET parameter “resource” and sinks the domain directly into the hardcoded Webfinger URL (“{protocol}://{domain}/.well-known/webfinger?resource=acct:{identifier}”) without any additional checks.\nAfterwards the user domain input will pass into the “fetch_object_http_with_accept” function and finally into the security check on “verify_url_valid” function, again, without any form of sanitizing or input validation.\nAn adversary can cause unwanted behaviours using multiple techniques:\n\n1. **_Gaining control over the query’s path:_**\nAn adversary can manipulate the Webfinger hard-coded URL, gaining full control over the GET request domain, path and port by submitting malicious input like: hacker@hacker_host:1337/hacker_path?hacker_param#, which in turn will result in the following string:\nhttp[s]://hacker_host:1337/hacker_path?hacker_param#/.well-known/webfinger?resource=acct:{identifier}, directing the URL into another domain and path without any issues as the hash character renders the rest of the URL path unrecognized by the webserver.\n\n2. **_Bypassing the domain’s restriction using DNS resolving mechanism:_**\nAn adversary can manipulate the security check and force it to look for internal services regardless the Localhost check by using a domain name that resolves into a local IP (such as: localh.st, for example), as the security check does not verify the resolved IP at all - any service under the Localhost domain can be reached.\n\n3. _**Bypassing the domain’s restriction using official Fully Qualified Domain Names (FQDNs):**_\nIn the official DNS specifications, a fully qualified domain name actually should end with a dot.\nWhile most of the time a domain name is presented without any trailing dot, the resolver will assume it exists, however - it is still possible to use a domain name with a trailing dot which will resolve correctly.\nAs the Localhost check is mainly a simple comparison check - if we register a “hacker@localhost.” domain it will pass the test as “localhost” is not equal to “localhost.”, however the domain will be valid (Using this mechanism it is also possible to bypass any domain blocklist mechanism).\n\n\n### PoC\n\n1. Activate a local HTTP server listening to port 1234 with a “secret.txt” file:\n`python3 -m http.server 1234`\n2. Open the “main.rs” file inside the “example” folder on the activitypub-federated-rust project, and modify the “beta@localhost” string into “hacker@localh.st:1234/secret.txt?something=1#”.\n3. Run the example using the following command:\n`cargo run --example local_federation axum`\n4. View the console of the Python’s HTTP server and see that a request for a “secret.txt” file was performed.\n\nThis proves that we can redirect the URL to any domain and path we choose.\nNow on the next steps we will prove that the security checks of Localhost and blocked domains can be easily bypassed (both checks use the same comparison mechanism).\n\n1. Now open the “instance.rs” file inside the “example” folder and view that the domain “malicious.com” is blocked (you can switch it to any desired domain address).\n2. Change the same “beta@localhost” string into “hacker@malicious.com” and run the example command to see that the malicious domain blocking mechanism is working as expected.\n3. Now change the “hacker@malicious.com” string into  “hacker@malicious.com.” string and re-initiate the example, view now that the check passed successfully.\n4. You can combine both methods on “localhost.” domain (or any other domain) to verify that the FQDNs resolving is indeed successful.\n\n\n### Impact\nDue to this issue, any user can cause the server to send GET requests with controlled path and port in an attempt to query services running on the instance’s host, and attempt to execute a Blind-SSRF gadget in hope of targeting a known vulnerable local service running on the victim’s machine.\n\n### Fix Suggestion\nModify the domain validation mechanism and implement the following checks:\n\n1. Resolve the domain and validate it is not using any invalid IP address (internal, or cloud metadata IPs) using regexes of both IPv4 and IPv6 addresses.\nFor Implementation example of a good SSRF prevention practice you can review a similiar project such as “Fedify” (https://github.com/dahlia/fedify/blob/main/src/runtime/url.ts) which handles external URL resource correctly.\nNote that it is still needed to remove unwanted characters from the URL. \n2. Filter the user’s input for any unwanted characters that should not be present on a domain name, such as #,?,/, etc.\n3. Perform checks that make sure the desired request path is the executed path with the same port.\n4. Disable automatic HTTP redirect follows on the implemented client, as redirects can be used for security mechanisms circumvention.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:L/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "crates.io",
      "name" : "activitypub_federation"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "last_affected" : "0.6.2"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/LemmyNet/lemmy/security/advisories/GHSA-7723-35v7-qcxw"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2025-25194"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/LemmyNet/activitypub-federation-rust"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-918" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2025-02-10T20:25:37Z",
    "nvd_published_at" : "2025-02-10T23:15:16Z"
  }
}