VYPR
High severity7.5OSV Advisory· Published Nov 7, 2025· Updated Apr 15, 2026

CVE-2025-64347

CVE-2025-64347

Description

Apollo Router Core is a configurable Rust graph router written to run a federated supergraph using Apollo Federation 2. Versions 1.61.12-rc.0 and below and 2.8.1-rc.0 allow unauthorized access to protected data through schema elements with access control directives (@authenticated, @requiresScopes, and @policy) that were renamed via @link imports. Router did not enforce renamed access control directives on schema elements (e.g. fields and types), allowing queries to bypass those element-level access controls. This issue is fixed in versions 1.61.12 and 2.8.1.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
apollo-routercrates.io
< 1.61.121.61.12
apollo-routercrates.io
>= 2.0.0-alpha.0, < 2.8.12.8.1

Affected products

1

Patches

1
78e4b20a2fc2

fix: update auth plugin handling of directive renames

https://github.com/apollographql/routerSachin D. ShindeOct 21, 2025via ghsa
9 files changed · +118 115
  • apollo-router/src/plugins/authorization/authenticated.rs+12 7 modified
    @@ -6,8 +6,10 @@ use apollo_compiler::Name;
     use apollo_compiler::Node;
     use apollo_compiler::ast;
     use apollo_compiler::executable;
    +use apollo_compiler::name;
     use apollo_compiler::schema;
     use apollo_compiler::schema::Implementers;
    +use apollo_federation::link::spec::Identity;
     use tower::BoxError;
     
     use crate::json_ext::Path;
    @@ -18,8 +20,7 @@ use crate::spec::query::transform;
     use crate::spec::query::transform::TransformState;
     use crate::spec::query::traverse;
     
    -pub(crate) const AUTHENTICATED_DIRECTIVE_NAME: &str = "authenticated";
    -pub(crate) const AUTHENTICATED_SPEC_BASE_URL: &str = "https://specs.apollo.dev/authenticated";
    +pub(crate) const AUTHENTICATED_DIRECTIVE_NAME: Name = name!("authenticated");
     pub(crate) const AUTHENTICATED_SPEC_VERSION_RANGE: &str = ">=0.1.0, <=0.1.0";
     
     pub(crate) struct AuthenticatedCheckVisitor<'a> {
    @@ -43,9 +44,9 @@ impl<'a> AuthenticatedCheckVisitor<'a> {
                 found: false,
                 authenticated_directive_name: Schema::directive_name(
                     schema,
    -                AUTHENTICATED_SPEC_BASE_URL,
    +                &Identity::authenticated_identity(),
                     AUTHENTICATED_SPEC_VERSION_RANGE,
    -                AUTHENTICATED_DIRECTIVE_NAME,
    +                &AUTHENTICATED_DIRECTIVE_NAME,
                 )?,
             })
         }
    @@ -204,9 +205,9 @@ impl<'a> AuthenticatedVisitor<'a> {
                 current_path: Path::default(),
                 authenticated_directive_name: Schema::directive_name(
                     schema,
    -                AUTHENTICATED_SPEC_BASE_URL,
    +                &Identity::authenticated_identity(),
                     AUTHENTICATED_SPEC_VERSION_RANGE,
    -                AUTHENTICATED_DIRECTIVE_NAME,
    +                &AUTHENTICATED_DIRECTIVE_NAME,
                 )?,
             })
         }
    @@ -1104,7 +1105,11 @@ mod tests {
         schema
           @link(url: "https://specs.apollo.dev/link/v1.0")
           @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
    -      @link(url: "https://specs.apollo.dev/authenticated/v0.1", as: "auth", for: SECURITY)
    +      @link(
    +        url: "https://specs.apollo.dev/authenticated/v0.1"
    +        import: [{ name: "@authenticated", as: "@auth" }]
    +        for: SECURITY
    +      )
         {
           query: Query
           mutation: Mutation
    
  • apollo-router/src/plugins/authorization/mod.rs+4 6 modified
    @@ -6,6 +6,7 @@ use std::ops::ControlFlow;
     
     use apollo_compiler::ExecutableDocument;
     use apollo_compiler::ast;
    +use apollo_federation::link::spec::Identity;
     use http::StatusCode;
     use schemars::JsonSchema;
     use serde::Deserialize;
    @@ -15,15 +16,12 @@ use tower::BoxError;
     use tower::ServiceBuilder;
     use tower::ServiceExt;
     
    -use self::authenticated::AUTHENTICATED_SPEC_BASE_URL;
     use self::authenticated::AUTHENTICATED_SPEC_VERSION_RANGE;
     use self::authenticated::AuthenticatedCheckVisitor;
     use self::authenticated::AuthenticatedVisitor;
    -use self::policy::POLICY_SPEC_BASE_URL;
     use self::policy::POLICY_SPEC_VERSION_RANGE;
     use self::policy::PolicyExtractionVisitor;
     use self::policy::PolicyFilteringVisitor;
    -use self::scopes::REQUIRES_SCOPES_SPEC_BASE_URL;
     use self::scopes::REQUIRES_SCOPES_SPEC_VERSION_RANGE;
     use self::scopes::ScopeExtractionVisitor;
     use self::scopes::ScopeFilteringVisitor;
    @@ -153,13 +151,13 @@ impl AuthorizationPlugin {
                 .and_then(|v| v.get("enabled").and_then(|v| v.as_bool()));
     
             let has_authorization_directives = schema.has_spec(
    -            AUTHENTICATED_SPEC_BASE_URL,
    +            &Identity::authenticated_identity(),
                 AUTHENTICATED_SPEC_VERSION_RANGE,
             ) || schema.has_spec(
    -            REQUIRES_SCOPES_SPEC_BASE_URL,
    +            &Identity::requires_scopes_identity(),
                 REQUIRES_SCOPES_SPEC_VERSION_RANGE,
             ) || schema
    -            .has_spec(POLICY_SPEC_BASE_URL, POLICY_SPEC_VERSION_RANGE);
    +            .has_spec(&Identity::policy_identity(), POLICY_SPEC_VERSION_RANGE);
     
             Ok(has_config.unwrap_or(true) && has_authorization_directives)
         }
    
  • apollo-router/src/plugins/authorization/policy.rs+12 7 modified
    @@ -13,8 +13,10 @@ use apollo_compiler::Name;
     use apollo_compiler::Node;
     use apollo_compiler::ast;
     use apollo_compiler::executable;
    +use apollo_compiler::name;
     use apollo_compiler::schema;
     use apollo_compiler::schema::Implementers;
    +use apollo_federation::link::spec::Identity;
     use tower::BoxError;
     
     use crate::json_ext::Path;
    @@ -33,8 +35,7 @@ pub(crate) struct PolicyExtractionVisitor<'a> {
         entity_query: bool,
     }
     
    -pub(crate) const POLICY_DIRECTIVE_NAME: &str = "policy";
    -pub(crate) const POLICY_SPEC_BASE_URL: &str = "https://specs.apollo.dev/policy";
    +pub(crate) const POLICY_DIRECTIVE_NAME: Name = name!("policy");
     pub(crate) const POLICY_SPEC_VERSION_RANGE: &str = ">=0.1.0, <=0.1.0";
     
     impl<'a> PolicyExtractionVisitor<'a> {
    @@ -51,9 +52,9 @@ impl<'a> PolicyExtractionVisitor<'a> {
                 extracted_policies: HashSet::new(),
                 policy_directive_name: Schema::directive_name(
                     schema,
    -                POLICY_SPEC_BASE_URL,
    +                &Identity::policy_identity(),
                     POLICY_SPEC_VERSION_RANGE,
    -                POLICY_DIRECTIVE_NAME,
    +                &POLICY_DIRECTIVE_NAME,
                 )?,
             })
         }
    @@ -238,9 +239,9 @@ impl<'a> PolicyFilteringVisitor<'a> {
                 current_path: Path::default(),
                 policy_directive_name: Schema::directive_name(
                     schema,
    -                POLICY_SPEC_BASE_URL,
    +                &Identity::policy_identity(),
                     POLICY_SPEC_VERSION_RANGE,
    -                POLICY_DIRECTIVE_NAME,
    +                &POLICY_DIRECTIVE_NAME,
                 )?,
             })
         }
    @@ -1526,7 +1527,11 @@ mod tests {
           schema
             @link(url: "https://specs.apollo.dev/link/v1.0")
             @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
    -        @link(url: "https://specs.apollo.dev/policy/v0.1", as: "policies" for: SECURITY)
    +        @link(
    +          url: "https://specs.apollo.dev/policy/v0.1"
    +          import: [{ name: "@policy", as: "@policies" }]
    +          for: SECURITY
    +        )
           {
               query: Query
               mutation: Mutation
    
  • apollo-router/src/plugins/authorization/scopes.rs+12 7 modified
    @@ -13,8 +13,10 @@ use apollo_compiler::Name;
     use apollo_compiler::Node;
     use apollo_compiler::ast;
     use apollo_compiler::executable;
    +use apollo_compiler::name;
     use apollo_compiler::schema;
     use apollo_compiler::schema::Implementers;
    +use apollo_federation::link::spec::Identity;
     use tower::BoxError;
     
     use crate::json_ext::Path;
    @@ -33,8 +35,7 @@ pub(crate) struct ScopeExtractionVisitor<'a> {
         entity_query: bool,
     }
     
    -pub(crate) const REQUIRES_SCOPES_DIRECTIVE_NAME: &str = "requiresScopes";
    -pub(crate) const REQUIRES_SCOPES_SPEC_BASE_URL: &str = "https://specs.apollo.dev/requiresScopes";
    +pub(crate) const REQUIRES_SCOPES_DIRECTIVE_NAME: Name = name!("requiresScopes");
     pub(crate) const REQUIRES_SCOPES_SPEC_VERSION_RANGE: &str = ">=0.1.0, <=0.1.0";
     
     impl<'a> ScopeExtractionVisitor<'a> {
    @@ -51,9 +52,9 @@ impl<'a> ScopeExtractionVisitor<'a> {
                 extracted_scopes: HashSet::new(),
                 requires_scopes_directive_name: Schema::directive_name(
                     schema,
    -                REQUIRES_SCOPES_SPEC_BASE_URL,
    +                &Identity::requires_scopes_identity(),
                     REQUIRES_SCOPES_SPEC_VERSION_RANGE,
    -                REQUIRES_SCOPES_DIRECTIVE_NAME,
    +                &REQUIRES_SCOPES_DIRECTIVE_NAME,
                 )?,
             })
         }
    @@ -237,9 +238,9 @@ impl<'a> ScopeFilteringVisitor<'a> {
                 current_path: Path::default(),
                 requires_scopes_directive_name: Schema::directive_name(
                     schema,
    -                REQUIRES_SCOPES_SPEC_BASE_URL,
    +                &Identity::requires_scopes_identity(),
                     REQUIRES_SCOPES_SPEC_VERSION_RANGE,
    -                REQUIRES_SCOPES_DIRECTIVE_NAME,
    +                &REQUIRES_SCOPES_DIRECTIVE_NAME,
                 )?,
             })
         }
    @@ -1384,7 +1385,11 @@ mod tests {
         schema
           @link(url: "https://specs.apollo.dev/link/v1.0")
           @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
    -      @link(url: "https://specs.apollo.dev/requiresScopes/v0.1", as: "scopes" for: SECURITY)
    +      @link(
    +        url: "https://specs.apollo.dev/requiresScopes/v0.1"
    +        import: [{ name: "@requiresScopes", as: "@scopes" }]
    +        for: SECURITY
    +      )
         {
             query: Query
             mutation: Mutation
    
  • apollo-router/src/plugins/progressive_override/mod.rs+6 4 modified
    @@ -2,9 +2,12 @@ use std::collections::HashMap;
     use std::collections::HashSet;
     use std::sync::Arc;
     
    +use apollo_compiler::Name;
     use apollo_compiler::Schema;
    +use apollo_compiler::name;
     use apollo_compiler::schema::ExtendedType;
     use apollo_compiler::validation::Valid;
    +use apollo_federation::link::spec::Identity;
     use dashmap::DashMap;
     use schemars::JsonSchema;
     use serde::Deserialize;
    @@ -27,8 +30,7 @@ pub(crate) mod visitor;
     pub(crate) const UNRESOLVED_LABELS_KEY: &str = "apollo::progressive_override::unresolved_labels";
     pub(crate) const LABELS_TO_OVERRIDE_KEY: &str = "apollo::progressive_override::labels_to_override";
     
    -pub(crate) const JOIN_FIELD_DIRECTIVE_NAME: &str = "join__field";
    -pub(crate) const JOIN_SPEC_BASE_URL: &str = "https://specs.apollo.dev/join";
    +pub(crate) const JOIN_FIELD_DIRECTIVE_NAME: Name = name!("field");
     pub(crate) const JOIN_SPEC_VERSION_RANGE: &str = ">=0.4";
     pub(crate) const OVERRIDE_LABEL_ARG_NAME: &str = "overrideLabel";
     
    @@ -57,9 +59,9 @@ type LabelsFromSchema = (
     fn collect_labels_from_schema(schema: &Schema) -> LabelsFromSchema {
         let Some(join_field_directive_name_in_schema) = spec::Schema::directive_name(
             schema,
    -        JOIN_SPEC_BASE_URL,
    +        &Identity::join_identity(),
             JOIN_SPEC_VERSION_RANGE,
    -        JOIN_FIELD_DIRECTIVE_NAME,
    +        &JOIN_FIELD_DIRECTIVE_NAME,
         ) else {
             tracing::debug!("No join spec >=v0.4 found in the schema. No labels will be overridden.");
             return (Arc::new(HashMap::new()), Arc::new(HashSet::new()));
    
  • apollo-router/src/plugins/progressive_override/tests.rs+33 13 modified
    @@ -1,6 +1,7 @@
     use std::sync::Arc;
     
     use apollo_compiler::Schema;
    +use apollo_federation::link::spec::Identity;
     use tower::ServiceExt;
     
     use crate::Context;
    @@ -12,7 +13,6 @@ use crate::plugin::test::MockRouterService;
     use crate::plugin::test::MockSupergraphService;
     use crate::plugins::progressive_override::Config;
     use crate::plugins::progressive_override::JOIN_FIELD_DIRECTIVE_NAME;
    -use crate::plugins::progressive_override::JOIN_SPEC_BASE_URL;
     use crate::plugins::progressive_override::JOIN_SPEC_VERSION_RANGE;
     use crate::plugins::progressive_override::LABELS_TO_OVERRIDE_KEY;
     use crate::plugins::progressive_override::ProgressiveOverridePlugin;
    @@ -30,22 +30,42 @@ const SCHEMA_NO_USAGES: &str = include_str!("testdata/supergraph_no_usages.graph
     fn test_progressive_overrides_are_recognised_vor_join_v0_4_and_above() {
         let schema_for_version = |version| {
             format!(
    -            r#"schema
    -                @link(url: "https://specs.apollo.dev/link/v1.0")
    -                @link(url: "https://specs.apollo.dev/join/{version}", for: EXECUTION)
    -                @link(url: "https://specs.apollo.dev/context/v0.1", for: SECURITY)
    -
    -                directive @join__field repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION"#
    +            r#"
    +            schema
    +              @link(url: "https://specs.apollo.dev/link/v1.0")
    +              @link(url: "https://specs.apollo.dev/join/{version}", for: EXECUTION)
    +              @link(url: "https://specs.apollo.dev/context/v0.1", for: SECURITY)
    +
    +            directive @link(
    +              url: String
    +              as: String
    +              for: link__Purpose
    +              import: [link__Import]
    +            ) repeatable on SCHEMA
    +            scalar link__Import
    +            enum link__Purpose {{
    +              """
    +              `SECURITY` features provide metadata necessary to securely resolve fields.
    +              """
    +              SECURITY
    +
    +              """
    +              `EXECUTION` features provide metadata necessary for operation execution.
    +              """
    +              EXECUTION
    +            }}
    +            directive @join__field repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
    +            "#
             )
         };
     
         let join_v3_schema = Schema::parse(schema_for_version("v0.3"), "test").unwrap();
         assert!(
             crate::spec::Schema::directive_name(
                 &join_v3_schema,
    -            JOIN_SPEC_BASE_URL,
    +            &Identity::join_identity(),
                 JOIN_SPEC_VERSION_RANGE,
    -            JOIN_FIELD_DIRECTIVE_NAME,
    +            &JOIN_FIELD_DIRECTIVE_NAME,
             )
             .is_none()
         );
    @@ -54,9 +74,9 @@ fn test_progressive_overrides_are_recognised_vor_join_v0_4_and_above() {
         assert!(
             crate::spec::Schema::directive_name(
                 &join_v4_schema,
    -            JOIN_SPEC_BASE_URL,
    +            &Identity::join_identity(),
                 JOIN_SPEC_VERSION_RANGE,
    -            JOIN_FIELD_DIRECTIVE_NAME,
    +            &JOIN_FIELD_DIRECTIVE_NAME,
             )
             .is_some()
         );
    @@ -66,9 +86,9 @@ fn test_progressive_overrides_are_recognised_vor_join_v0_4_and_above() {
         assert!(
             crate::spec::Schema::directive_name(
                 &join_v5_schema,
    -            JOIN_SPEC_BASE_URL,
    +            &Identity::join_identity(),
                 JOIN_SPEC_VERSION_RANGE,
    -            JOIN_FIELD_DIRECTIVE_NAME,
    +            &JOIN_FIELD_DIRECTIVE_NAME,
             )
             .is_some()
         )
    
  • apollo-router/src/plugins/progressive_override/visitor.rs+3 3 modified
    @@ -5,10 +5,10 @@ use std::sync::Arc;
     use apollo_compiler::ast;
     use apollo_compiler::executable;
     use apollo_compiler::schema;
    +use apollo_federation::link::spec::Identity;
     use tower::BoxError;
     
     use super::JOIN_FIELD_DIRECTIVE_NAME;
    -use super::JOIN_SPEC_BASE_URL;
     use super::JOIN_SPEC_VERSION_RANGE;
     use super::OVERRIDE_LABEL_ARG_NAME;
     use crate::spec::Schema;
    @@ -21,9 +21,9 @@ impl<'a> OverrideLabelVisitor<'a> {
                 override_labels: HashSet::new(),
                 join_field_directive_name: Schema::directive_name(
                     schema,
    -                JOIN_SPEC_BASE_URL,
    +                &Identity::join_identity(),
                     JOIN_SPEC_VERSION_RANGE,
    -                JOIN_FIELD_DIRECTIVE_NAME,
    +                &JOIN_FIELD_DIRECTIVE_NAME,
                 )?,
             })
         }
    
  • apollo-router/src/spec/schema.rs+29 68 modified
    @@ -14,6 +14,8 @@ use apollo_federation::Supergraph;
     use apollo_federation::connectors::expand::Connectors;
     use apollo_federation::connectors::expand::ExpansionResult;
     use apollo_federation::connectors::expand::expand_connectors;
    +use apollo_federation::link::database::links_metadata;
    +use apollo_federation::link::spec::Identity;
     use apollo_federation::router_supported_supergraph_specs;
     use apollo_federation::schema::ValidFederationSchema;
     use http::Uri;
    @@ -300,80 +302,39 @@ impl Schema {
             None
         }
     
    -    pub(crate) fn has_spec(&self, base_url: &str, expected_version_range: &str) -> bool {
    -        self.supergraph_schema()
    -            .schema_definition
    -            .directives
    -            .iter()
    -            .filter(|dir| dir.name.as_str() == "link")
    -            .any(|link| {
    -                if let Some(url_in_link) = link
    -                    .specified_argument_by_name("url")
    -                    .and_then(|value| value.as_str())
    -                {
    -                    let Some((base_url_in_link, version_in_link)) = url_in_link.rsplit_once("/v")
    -                    else {
    -                        return false;
    -                    };
    -
    -                    let Some(version_in_url) =
    -                        Version::parse(format!("{version_in_link}.0").as_str()).ok()
    -                    else {
    -                        return false;
    -                    };
    -
    -                    let Some(version_range) = VersionReq::parse(expected_version_range).ok() else {
    -                        return false;
    -                    };
    -
    -                    base_url_in_link == base_url && version_range.matches(&version_in_url)
    -                } else {
    -                    false
    -                }
    -            })
    +    /// This function assumes `@link` usage is valid in the schema, and will return `false` if not.
    +    pub(crate) fn has_spec(&self, spec_identity: &Identity, expected_version_range: &str) -> bool {
    +        let Ok(Some(metadata)) = links_metadata(self.supergraph_schema()) else {
    +            return false;
    +        };
    +        let Some(link) = metadata.for_identity(spec_identity) else {
    +            return false;
    +        };
    +        let Some(semver_version) = Version::parse(format!("{}.0", link.url.version).as_str()).ok()
    +        else {
    +            return false;
    +        };
    +        let Some(version_range) = VersionReq::parse(expected_version_range).ok() else {
    +            return false;
    +        };
    +        version_range.matches(&semver_version)
         }
     
    +    /// This function assumes `@link` usage is valid in the schema, and will return `None` if not.
         pub(crate) fn directive_name(
             schema: &apollo_compiler::schema::Schema,
    -        base_url: &str,
    +        spec_identity: &Identity,
             expected_version_range: &str,
    -        default: &str,
    +        default: &Name,
         ) -> Option<String> {
    -        schema
    -            .schema_definition
    -            .directives
    -            .iter()
    -            .filter(|dir| dir.name.as_str() == "link")
    -            .find(|link| {
    -                if let Some(url_in_link) = link
    -                    .specified_argument_by_name("url")
    -                    .and_then(|value| value.as_str())
    -                {
    -                    let Some((base_url_in_link, version_in_link)) = url_in_link.rsplit_once("/v")
    -                    else {
    -                        return false;
    -                    };
    -
    -                    let Some(version_in_url) =
    -                        Version::parse(format!("{version_in_link}.0").as_str()).ok()
    -                    else {
    -                        return false;
    -                    };
    -
    -                    let Some(version_range) = VersionReq::parse(expected_version_range).ok() else {
    -                        return false;
    -                    };
    -
    -                    base_url_in_link == base_url && version_range.matches(&version_in_url)
    -                } else {
    -                    false
    -                }
    -            })
    -            .map(|link| {
    -                link.specified_argument_by_name("as")
    -                    .and_then(|value| value.as_str().map(|s| s.to_string()))
    -                    .unwrap_or_else(|| default.to_string())
    -            })
    +        let metadata = links_metadata(schema).ok()??;
    +        let link = metadata.for_identity(spec_identity)?;
    +        let semver_version = Version::parse(format!("{}.0", link.url.version).as_str()).ok()?;
    +        let version_range = VersionReq::parse(expected_version_range).ok()?;
    +        if !version_range.matches(&semver_version) {
    +            return None;
    +        }
    +        Some(link.directive_name_in_schema(default).to_string())
         }
     }
     
    
  • .changesets/fix_puddle_sample_register_dig.md+7 0 added
    @@ -0,0 +1,7 @@
    +### Fixed authorization plugin handling of directive renames
    +
    +The router authorization plugin did not properly handle authorization requirements when subgraphs renamed their authentication directives through imports. When such renames occurred, the plugin’s `@link`-processing code ignored the imported directives entirely, causing authentication constraints defined by the renamed directives to be ignored.
    +
    +The plugin code was updated to call the appropriate functionality in the `apollo-federation` crate, which correctly handles both because spec and imports directive renames.
    +
    +By [@sachindshinde](https://github.com/sachindshinde) in https://github.com/apollographql/router/pull/PULL_NUMBER
    

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

4

News mentions

0

No linked articles in our index yet.