Overview
CVE-2021-26077 describes a broken authentication issue in Atlassian Connect Spring Boot (ACSB) where certain lifecycle endpoints could accept context JWTs even though only server-to-server (S2S) JWTs should be valid for those operations. This allowed an attacker to send authenticated re-installation events to an Atlassian Connect Spring Boot app, effectively impersonating legitimate installation or lifecycle actions. The vulnerability affected ACSB versions 1.1.0 before 2.1.3 and 2.1.4 before 2.1.5, enabling an attacker to bypass the intended authentication model and trigger privileged lifecycle flows. In the Spring Boot context, apps that rely on ACSB for JWT validation could treat context tokens as if they were S2S tokens, granting unauthorized access to install/uninstall or reconfigure events. The fix requires ensuring that lifecycle endpoints strictly validate tokens as S2S tokens and reject context tokens, or upgrading to a patched ACSB release that enforces this separation. This class of vulnerability is a form of Broken Authentication (CWE-287) and undermines the trust boundary between Atlassian products and the Connected app, which is critical in Spring Boot services that expose install/lifecycle webhooks or controllers.
In practice, Spring Boot apps using ACSB should upgrade to the patched ACSB (2.1.5 or later) or implement explicit token-type checks to ensure that only S2S tokens are accepted for lifecycle endpoints. The real-world impact is substantial: an attacker could manipulate installation lifecycles, potentially gaining persistent access or misconfiguring integrations. This guide demonstrates how the vulnerability manifests in code and provides a concrete remediation approach with a side-by-side code example showing the vulnerable pattern and the fix.
Affected Versions
1.1.0 before 2.1.3 and 2.1.4 before 2.1.5
Code Fix Example
Spring Boot API Security Remediation
package com.example.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
// Vulnerable pattern: accepts any valid JWT, including context tokens, for all endpoints
public class VulnerableJwtAuthenticationFilter extends OncePerRequestFilter {
private static final String SECRET = "change_me_to_your_actual_signing_key";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Claims claims = Jwts
.parser()
.setSigningKey(SECRET.getBytes())
.parseClaimsJws(token)
.getBody();
String subject = claims.getSubject();
// No check on token_type; context tokens are treated as S2S
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(subject, null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (Exception ignored) {
// Invalid token -> continue without authentication
}
}
filterChain.doFilter(request, response);
}
}
// Secure pattern: explicitly enforce S2S-only tokens on lifecycle endpoints
public class SecureJwtAuthenticationFilter extends OncePerRequestFilter {
private static final String SECRET = "change_me_to_your_actual_signing_key";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Claims claims = Jwts
.parser()
.setSigningKey(SECRET.getBytes())
.parseClaimsJws(token)
.getBody();
String tokenType = claims.get("token_type", String.class);
String path = request.getRequestURI();
boolean isLifecycleEndpoint = path.startsWith("/lifecycle");
// If the endpoint is a lifecycle operation, require an S2S token
if (isLifecycleEndpoint && (tokenType == null || !"s2s".equalsIgnoreCase(tokenType))) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
String subject = claims.getSubject();
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(subject, null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (Exception e) {
// Invalid token
}
}
filterChain.doFilter(request, response);
}
}