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.
| Package | Affected versions | Patched versions |
|---|---|---|
apollo-routercrates.io | < 1.61.12 | 1.61.12 |
apollo-routercrates.io | >= 2.0.0-alpha.0, < 2.8.1 | 2.8.1 |
Affected products
1- Range: v0.1.0-alpha.0, v0.1.0-alpha.1, v0.1.0-alpha.10, …
Patches
178e4b20a2fc2fix: update auth plugin handling of directive renames
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
4News mentions
0No linked articles in our index yet.