VYPR
Medium severity6.3NVD Advisory· Published May 8, 2026· Updated May 13, 2026

CVE-2026-42180

CVE-2026-42180

Description

Lemmy is a link aggregator and forum for the fediverse. Prior to version 0.19.18, Lemmy allows an authenticated low-privileged user to create a link post through POST /api/v3/post. When a post is created in a public community, the backend asynchronously sends a Webmention to the attacker-controlled link target. The submitted URL is checked for syntax and scheme, but the audited code path does not reject loopback, private, or link-local destinations before the Webmention request is issued. This lets a normal user trigger server-side HTTP requests toward internal services. This issue has been patched in version 0.19.18.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
lemmy_api_commoncrates.io
< 0.19.180.19.18

Patches

1
1f06693b7080

[0.19] Add IP check for webmention (#6444)

https://github.com/LemmyNet/lemmyNutomicApr 15, 2026via ghsa
2 files changed · +35 21
  • crates/api_common/src/request.rs+33 19 modified
    @@ -37,7 +37,7 @@ use serde::{Deserialize, Serialize};
     use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
     use tokio::net::lookup_host;
     use tracing::{info, warn};
    -use url::Url;
    +use url::{Host, Url};
     use urlencoding::encode;
     use webpage::HTML;
     
    @@ -64,24 +64,7 @@ pub async fn fetch_link_metadata(
         return Err(LemmyErrorType::InvalidUrl.into());
       }
     
    -  // Resolve the domain and throw an error if it points to any internal IP,
    -  // using logic from nightly IpAddr::is_global.
    -  if !cfg!(debug_assertions) {
    -    // TODO: Replace with IpAddr::is_global() once stabilized
    -    //       https://doc.rust-lang.org/std/net/enum.IpAddr.html#method.is_global
    -    let domain = url.domain().ok_or(LemmyErrorType::UrlWithoutDomain)?;
    -    let invalid_ip =
    -      lookup_host((domain.to_owned(), 80))
    -        .await?
    -        .any(|addr| match addr.ip().to_canonical() {
    -          IpAddr::V4(addr) => v4_is_invalid(addr),
    -          IpAddr::V6(addr) => v6_is_invalid(addr),
    -        });
    -    if invalid_ip {
    -      return Err(LemmyErrorType::InvalidUrl.into());
    -    }
    -  }
    -
    +  validate_link_ip(url).await?;
       info!("Fetching site metadata for url: {}", url);
       // We only fetch the first MB of data in order to not waste bandwidth especially for large
       // binary files. This high limit is particularly needed for youtube, which includes a lot of
    @@ -161,6 +144,37 @@ pub async fn fetch_link_metadata(
       })
     }
     
    +/// Resolve the domain and throw an error if it points to any internal IP.
    +pub async fn validate_link_ip(url: &Url) -> LemmyResult<()> {
    +  if cfg!(debug_assertions) {
    +    return Ok(());
    +  }
    +  // Resolve the domain and throw an error if it points to any internal IP,
    +  // using logic from nightly IpAddr::is_global.
    +
    +  // TODO: Replace with IpAddr::is_global() once stabilized
    +  //       https://doc.rust-lang.org/std/net/enum.IpAddr.html#method.is_global
    +  let mut ip = vec![];
    +  match url.host().ok_or(LemmyErrorType::UrlWithoutDomain)? {
    +    Host::Domain(domain) => ip.extend(
    +      lookup_host((domain.to_owned(), 80))
    +        .await?
    +        .map(|s| s.ip().to_canonical()),
    +    ),
    +    Host::Ipv4(ipv4) => ip.push(ipv4.into()),
    +    Host::Ipv6(ipv6) => ip.push(ipv6.into()),
    +  };
    +
    +  let invalid_ip = ip.into_iter().any(|addr| match addr {
    +    IpAddr::V4(addr) => v4_is_invalid(addr),
    +    IpAddr::V6(addr) => v6_is_invalid(addr),
    +  });
    +  if invalid_ip {
    +    return Err(LemmyErrorType::InvalidUrl.into());
    +  }
    +  Ok(())
    +}
    +
     fn v4_is_invalid(v4: Ipv4Addr) -> bool {
       v4.is_private()
         || v4.is_loopback()
    
  • crates/api_crud/src/post/create.rs+2 2 modified
    @@ -4,7 +4,7 @@ use lemmy_api_common::{
       build_response::build_post_response,
       context::LemmyContext,
       post::{CreatePost, PostResponse},
    -  request::generate_post_link_metadata,
    +  request::{generate_post_link_metadata, validate_link_ip},
       send_activity::SendActivityData,
       utils::{
         check_community_user_action,
    @@ -178,7 +178,7 @@ pub async fn create_post(
       mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
     
       if let Some(url) = inserted_post.url.clone() {
    -    if community.visibility == CommunityVisibility::Public {
    +    if community.visibility == CommunityVisibility::Public && validate_link_ip(&url).await.is_ok() {
           spawn_try_task(async move {
             let mut webmention =
               Webmention::new::<Url>(inserted_post.ap_id.clone().into(), url.clone().into())?;
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.