Java Spring Projelerinde Güvenlik Kod İncelemesine Nereden Başlanır?
Java Spring projelerinde güvenlik kod incelemesi yalnızca controller veya SecurityConfig dosyasını okumakla başlamaz. Build graph, aktif profile, filter chain, authorization modeli, data binding ve runtime configuration birlikte incelenmelidir.
SECNODEX Güvenlik Ekibi
Ofansif Güvenlik
Reviewer, yüz binlerce satırdan oluşan bir Spring Boot repository'sini açıyor.
İlk olarak SecurityConfig dosyasını buluyor. anyRequest().authenticated() satırını görüyor. Ardından repository genelinde permitAll, csrf().disable() ve NoOpPasswordEncoder arıyor. Açıkça tehlikeli görünen bir kullanım bulamayınca uygulamanın güvenlik temelinin güçlü olduğu sonucuna varıyor.
Oysa aynı projede üç kritik problem bulunuyor:
@EnableMethodSecurityhiç etkinleştirilmemiş- Order endpoint'i
tenantIddeğerini authenticated context yerine request body'den alıyor - Bir internal
SecurityFilterChain, yanlışsecurityMatchernedeniyle bazı management endpoint'lerini hiçbir chain ile eşleştirmiyor
Kodda klasik anlamda “tehlikeli function” bulunmuyor. Authentication çalışıyor. Endpoint'ler geçerli token istiyor. Yine de bir tenant başka tenant'a ait veriyi okuyabiliyor ve beklenmeyen bir management surface dışarıdan erişilebilir durumda.
İnceleme yanlış dosyadan değil, yanlış sorudan başladı.
Kısa cevap
Java Spring projesinde güvenlik kod incelemesine tek tek vulnerability arayarak başlanmaz. Önce incelenen commit, build graph, Spring generation, aktif profile, application türü, entry point'ler, trust boundary'ler ve security control yerleşimi belirlenir. Daha sonra SecurityFilterChain, method security, object-level authorization, data binding, persistence, outbound request, file processing, serialization, cache, async flow ve runtime configuration birlikte izlenir.
Spring Security dependency'sinin classpath'te bulunması, bütün request'lerin doğru chain'den geçtiğini göstermez. Bir endpoint'in authenticated olması, kullanıcının istediği object üzerinde yetkili olduğunu kanıtlamaz. @Valid kullanılması mass assignment veya business rule problemini çözmez. JPA kullanılması bütün query'leri SQL injection'a karşı otomatik olarak güvenli hale getirmez.
Bu nedenle etkili bir Java Spring güvenlik kod incelemesi, framework checklist'i ile application context'i birleştirmelidir.
Bu yazıda rastgele dosya okuma yerine izlenebilir bir review yöntemi kuracağız. Hangi artifact'ların önce inceleneceğini, SecurityFilterChain sırasının nasıl yorumlanacağını, authorization'ın controller'dan repository'ye nasıl takip edileceğini ve hangi kod pattern'lerinin gerçek bir finding'e dönüşebileceğini teknik örneklerle ele alacağız.
Spring projesinde code review neden genel Java checklist'i ile sınırlı kalamaz?
Spring, güvenlik açısından iki farklı etkiye sahiptir.
Bir yandan authentication, authorization, CSRF, session management, password encoding, security header ve OAuth 2.0 Resource Server gibi birçok kontrol için olgun bileşenler sunar. Diğer yandan bu kontroller annotation, bean, auto-configuration, profile, filter order ve proxy davranışı üzerinden etkinleşir.
Yani source code'da görülen tek bir satırın etkisi, yalnızca o satırla belirlenmez.
Örneğin:
- Bir
SecurityFilterChainbean'i Spring Boot'un bazı security auto-configuration kararlarını değiştirir @PreAuthorizeannotation'ı ancak method security gerçekten etkinse çalışır- Aynı class içindeki self-invocation, proxy üzerinden geçmediği için method interceptor'ı devre dışı bırakabilir
- Birden fazla
SecurityFilterChainbulunduğunda ilk eşleşen chain uygulanır securityMatcherilerequestMatchersfarklı görevler üstlenir@Profile,@ConditionalOnPropertyve environment variable bir security bean'inin üretimde oluşup oluşmadığını değiştirebilir- Spring MVC ile WebFlux aynı security chain tipini kullanmaz
- HTTP filter'ları
@KafkaListener,@MessageMappingveya@Scheduledile başlayan flow'ları korumaz @Transactional,@Async, cache ve method security advisor sırası davranışı etkileyebilir
Bu özellikler nedeniyle reviewer'ın ilk amacı “riskli API bulmak” değil, uygulamanın gerçekte hangi security model ile ayağa kalktığını anlamak olmalıdır.
İncelemeye başlamadan önce review contract oluşturun
Kaynak kod analizi belirli bir artifact üzerinde yapılır. “Spring uygulamasını inceledik” cümlesi tek başına yeniden üretilebilir bir kanıt değildir.
En az şu bilgiler kaydedilmelidir:
- Repository ve module listesi
- Commit SHA veya release tag
- İncelenen branch
- Build sistemi ve wrapper sürümü
- JDK sürümü
- Spring Boot ve Spring Framework generation'ı
- Servlet, reactive veya hybrid stack
- Aktif profile'lar
- Runtime ve deployment modeli
- Kullanılan identity provider
- Authentication türleri
- User role ve authority modeli
- Tenant modeli
- Public ve internal entry point'ler
- İncelenen config repository'leri
- Hariç tutulan module ve generated code
- SAST ve SCA coverage'ı
- Test edilebilen build ile incelenen commit'in eşleşmesi
Örnek bir review manifest'i şu şekilde tutulabilir:
review:
project: customer-platform
repository: customer-platform-api
commit: 9d2f4a7
branch: release/2026-q3
reviewer_scope:
- api
- domain
- persistence
- messaging
excluded:
- generated-clients
- performance-fixtures
runtime:
jdk: 21
stack: spring-mvc
packaging: container
profiles:
- production
- oauth
deployment:
- kubernetes
identity:
authentication:
- oidc-jwt
issuer: corporate-idp
audiences:
- customer-platform-api
roles:
- CUSTOMER
- SUPPORT
- ADMIN
tenants: multi-tenant
entry_points:
- rest
- kafka
- scheduled-jobs
- actuator
evidence:
build_reproduced: true
deployed_artifact_digest_available: true
architecture_diagram_available: true
role_permission_matrix_available: trueBu manifest bürokratik bir ek değildir. Reviewer'ın gördüğü kod ile production'da çalışan artifact arasındaki bağı kurar.
Commit ile deployed artifact neden eşleşmeli?
Kaynak kodda düzeltilmiş bir kontrol production image'a girmemiş olabilir. Bunun tersi de mümkündür. İncelenen branch'te riskli görünen özellik, production build profile'ında bulunmayabilir.
Güçlü bir inceleme şu zinciri doğrulamaya çalışır:
repository commit
→ build input
→ dependency graph
→ generated artifact
→ container image digest
→ deployment revisionBu zincir yoksa raporda açık bir limitation bulunmalıdır. “Kodda yok” ile “çalışan sistemde yok” aynı iddia değildir.
Kaynak kod analizi ile runtime sızma testinin farklı kanıtlar üretmesinin nedenlerini Kaynak kod analizi ile sızma testi aynı problemi çözmez yazımızda ayrıntılı olarak ele alıyoruz.
Birinci başlangıç noktası: Build ve dependency graph
Reviewer'ın ilk açacağı dosya her zaman controller olmamalıdır. Çoğu Spring projesinde doğru başlangıç noktaları şunlardır:
pom.xml- Parent POM
settings.xmlbuild.gradlebuild.gradle.ktssettings.gradlesettings.gradle.kts- Maven Wrapper veya Gradle Wrapper dosyaları
Dockerfile- Build pipeline tanımı
- Helm chart veya deployment manifest'i
application.ymlapplication-*.ymlapplication.properties- Config server repository'si
Amaç yalnızca Spring sürümünü görmek değildir. Hangi kodun build'e girdiği, dependency sürümlerinin nereden çözüldüğü ve hangi plugin'lerin build sırasında çalıştığı anlaşılmalıdır.
Dependency declaration ile resolved dependency aynı değildir
pom.xml içinde doğrudan jackson-databind görünmemesi, artifact içinde Jackson bulunmadığı anlamına gelmez. Transitive dependency başka bir starter üzerinden gelebilir. Benzer şekilde developer bir library sürümünü yazmamış olsa da Spring Boot BOM ilgili sürümü yönetebilir.
Maven projelerinde şu çıktılar yararlıdır:
./mvnw help:effective-pom
./mvnw dependency:tree -Dscope=runtime
./mvnw dependency:tree -DverboseGradle projelerinde:
./gradlew dependencies --configuration runtimeClasspath
./gradlew dependencyInsight --dependency spring-security-core --configuration runtimeClasspath
./gradlew dependencyInsight --dependency jackson-databind --configuration runtimeClasspathBu komutlar yalnızca yetkilendirilmiş ve güvenilen repository üzerinde çalıştırılmalıdır. Maven ve Gradle build'i plugin, task ve custom script çalıştırabilir. Reviewer, yabancı bir repository'de wrapper veya build task çalıştırmadan önce bunları da kod olarak incelemelidir.
Build katmanında aranacak riskler
- Support dışındaki Spring veya Java generation'ı
- Spring Boot BOM'dan bağımsız elle override edilmiş Spring module sürümleri
- Birbiriyle uyumsuz Spring Security module sürümleri
- Dynamic version,
SNAPSHOTveya değişebilir artifact kullanımı - Kurum dışı ve doğrulanmamış Maven repository'leri
- Dependency confusion'a açık internal package naming
systemPathile eklenen local JAR- Source repository'de bulunmayan proprietary JAR
- Build sırasında internetten script veya binary indiren plugin
- Test dependency'sinin production runtime'a sızması
- Exclusion nedeniyle güvenlik düzeltmesi içeren transitive module'ün düşmesi
- Dependency lock veya artifact integrity kontrolünün bulunmaması
- Container image içinde JAR ile dependency report'un uyuşmaması
Spring Boot her release için test edilmiş bir dependency seti sunar. BOM içindeki sürümleri rastgele override etmek yalnızca compatibility problemi değil, güvenlik patch'lerinin tutarsız uygulanması riskini de doğurur.
SCA çıktısı yeterli midir?
Hayır. Software Composition Analysis, bilinen CVE ve dependency riskini görmek için gereklidir. Fakat şu sorulara tek başına cevap vermez:
- Vulnerable code path gerçekten reachable mı?
- Güvenlik patch'i davranış değişikliği gerektiriyor mu?
- Uygulama riskli configuration koşulunu sağlıyor mu?
- Dependency shading nedeniyle gerçek sürüm farklı mı?
- Aynı açığın custom implementation'ı uygulama kodunda tekrarlandı mı?
- Dependency güvenli olsa da uygulama API'yi güvensiz mi kullanıyor?
SCA, Secure Code Review'in bir girdisidir. Onun yerine geçmez.
Otomatik analiz ile uzman incelemesinin farkını Secure Code Review ile otomatik SAST taraması farkı yazımızda karşılaştırıyoruz.
İkinci başlangıç noktası: Uygulamanın çalışma modelini belirleyin
Bir Spring repository'sinde aynı anda farklı application türleri bulunabilir:
- Spring MVC REST API
- Server-side rendered MVC application
- Spring WebFlux API
- Spring Cloud Gateway
- GraphQL server
- WebSocket veya STOMP service
- Kafka veya RabbitMQ consumer
- Batch job
- Scheduler
- CLI veya maintenance worker
- Actuator management service
Her türün entry point'i ve security interception modeli farklıdır.
MVC mi, WebFlux mı?
spring-boot-starter-web genellikle Servlet stack'i, spring-boot-starter-webflux ise reactive stack'i işaret eder. Fakat iki dependency'nin birlikte bulunması tek başına uygulamanın reactive çalıştığını kanıtlamaz. Boot application type seçimi, explicit configuration ve test setup birlikte değerlendirilmelidir.
MVC tarafında tipik güvenlik bileşeni:
SecurityFilterChainWebFlux tarafında:
SecurityWebFilterChainReviewer yalnızca SecurityFilterChain arayıp WebFlux route'larını gözden kaçırabilir. Tersi de mümkündür.
HTTP dışındaki entry point'leri çıkarın
Repository genelinde aşağıdaki annotation ve API'ler attack surface inventory'si oluşturmak için iyi başlangıç noktalarıdır:
rg -n '@RestController|@Controller|@RequestMapping|@GetMapping|@PostMapping|@PutMapping|@PatchMapping|@DeleteMapping' src
rg -n 'RouterFunction|RouterFunctions|route\\(' src
rg -n '@QueryMapping|@MutationMapping|@SchemaMapping' src
rg -n '@MessageMapping|WebSocketHandler|ChannelInterceptor' src
rg -n '@KafkaListener|@RabbitListener|@JmsListener' src
rg -n '@Scheduled|CommandLineRunner|ApplicationRunner' src
rg -n '@Endpoint|@WebEndpoint|@ReadOperation|@WriteOperation|@DeleteOperation' srcBu aramalar finding üretmez. Yalnızca inceleme haritası oluşturur.
Örneğin OrderService.approve() hem REST controller hem Kafka consumer tarafından çağrılıyorsa authorization kontrolünün yalnızca controller üzerinde bulunması yeterli değildir. HTTP request authenticated olsa bile message consumer aynı service method'una farklı trust assumption ile ulaşabilir.
Üçüncü başlangıç noktası: Bütün security configuration'ı tek modelde birleştirin
Spring Security review sırasında yalnızca SecurityConfig.java okunmaz. Aşağıdaki bileşenlerin tamamı birlikte aranmalıdır:
rg -n 'SecurityFilterChain|SecurityWebFilterChain|WebSecurityCustomizer' src
rg -n '@EnableWebSecurity|@EnableWebFluxSecurity|@EnableMethodSecurity|@EnableReactiveMethodSecurity' src
rg -n 'AuthenticationProvider|AuthenticationManager|UserDetailsService|PasswordEncoder' src
rg -n 'JwtDecoder|JwtAuthenticationConverter|OpaqueTokenIntrospector' src
rg -n 'OncePerRequestFilter|GenericFilterBean|FilterRegistrationBean' src
rg -n 'CorsConfigurationSource|CorsFilter|csrf\\(|cors\\(' src
rg -n '@PreAuthorize|@PostAuthorize|@PreFilter|@PostFilter|@Secured|@RolesAllowed|@PermitAll' srcArdından şu bilgiler çıkarılmalıdır:
| Bileşen | Reviewer'ın kanıtlaması gereken soru |
|---|---|
SecurityFilterChain | Hangi request'ler bu chain ile eşleşiyor? |
@Order | Birden fazla chain hangi sırayla değerlendiriliyor? |
securityMatcher | Chain'in bütünü hangi request surface'i için geçerli? |
requestMatchers | Eşleşen chain içinde hangi authorization rule uygulanıyor? |
WebSecurityCustomizer | Hangi path'ler security filter'lardan tamamen çıkarılıyor? |
| Method security | Gerçekten etkin mi ve hangi annotation'lar destekleniyor? |
| Custom filter | Chain içinde doğru konumda mı ve fail-closed davranıyor mu? |
| Authentication provider | Credential ve account state nasıl doğrulanıyor? |
| JWT decoder | Signature, issuer, audience ve time claim'leri nasıl doğrulanıyor? |
| CORS | Origin policy nereden geliyor ve credential kullanılıyor mu? |
| CSRF | Hangi client ve credential modeline göre açık veya kapalı? |
| Session | Stateless, stateful veya hybrid davranış gerçekten tutarlı mı? |
SecurityFilterChain incelemesinde en sık kaçırılan problem
Birden fazla chain olduğunda Spring Security, request ile eşleşen ilk chain'i seçer. Chain içindeki authorization rule'ların sırası da önemlidir.
Örnek bir yapı:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfiguration {
@Bean
@Order(1)
SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception {
http
.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(EndpointRequest.to("health")).permitAll()
.anyRequest().hasAuthority("ACTUATOR_READ")
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
@Order(2)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(HttpMethod.GET, "/api/catalog/**").permitAll()
.requestMatchers("/api/admin/**").hasAuthority("admin")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
@Bean
@Order(3)
SecurityFilterChain fallbackChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().denyAll()
);
return http.build();
}
}Bu örnekte önemli fikir yalnızca denyAll kullanılması değildir. Management endpoint'leri, API ve beklenmeyen diğer request'ler ayrı matcher sınırlarıyla ele alınmıştır. Son chain, yeni eklenen fakat authorization rule yazılmayan route'un sessizce public kalmasını önler.
API chain'inde CSRF, yalnızca browser'ın otomatik taşıdığı cookie, Basic authentication veya benzeri ambient credential kabul etmeyen bearer-only model varsayımıyla kapatılmıştır. Bu varsayım doğrulanmadan aynı configuration kopyalanmamalıdır.
securityMatcher ile requestMatchers aynı şey değildir
securityMatcher, ilgiliSecurityFilterChainbean'inin request için aday olup olmadığını belirlerrequestMatchers, chain seçildikten sonra authorization kararını belirler
Reviewer bu iki seviyeyi karıştırırsa bir rule'ün gerçekte hiçbir request'e uygulanmadığını göremeyebilir.
Hiçbir chain eşleşmezse ne olur?
Request herhangi bir SecurityFilterChain ile eşleşmiyorsa Spring Security o request'i korumaz. Bu nedenle yalnızca dar securityMatcher kullanan birden fazla chain bulunuyorsa fallback davranışı ayrıca incelenmelidir.
web.ignoring() ile permitAll() neden farklıdır?
permitAll() request'in erişimine izin verir, fakat request security filter chain içinden geçmeye devam eder. WebSecurityCustomizer üzerinden ignore edilen path ise Spring Security filter'larından tamamen çıkar.
Static resource için bile bu ayrım security header, logging veya ortak filter davranışını etkileyebilir. Reviewer, ignore listesinin yanlışlıkla dynamic endpoint, upload dizini veya management path içerip içermediğini kontrol etmelidir.
Authentication review: “Token doğrulanıyor” demek yeterli değildir
Authentication incelemesi credential'ın varlığıyla bitmez. Token veya session'ın hangi güvenlik iddialarını taşıdığı ve bunların nasıl doğrulandığı araştırılmalıdır.
JWT kullanan Resource Server için kontrol listesi
- Signature gerçekten doğrulanıyor mu?
- Kabul edilen algorithm ve key source açık mı?
issclaim'i doğrulanıyor mu?audclaim'i uygulamanın kendi audience değeriyle doğrulanıyor mu?expvenbfkontrol ediliyor mu?- Clock skew sınırlı mı?
- JWK key rotation destekleniyor mu?
- Aynı issuer'ın farklı client'ları arasında token confusion mümkün mü?
- Authority mapping hangi claim'den yapılıyor?
- Scope ve role aynı kavram gibi mi ele alınıyor?
- Custom converter boş veya beklenmeyen claim'de fail-closed mu?
- Service account ile end-user token'ı ayrıştırılıyor mu?
- Tenant claim'i signed ve güvenilir kaynaktan mı geliyor?
Spring Security, uygun Resource Server configuration'ında issuer ve time claim validation'ı sağlar. Audience kontrolü ise uygulamanın beklediği audience ile açıkça yapılandırılmalıdır.
Örnek:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
audiences:
- customer-platform-apiReviewer configuration dosyasını görmekle yetinmemelidir. Production profile'ın gerçekten bu değeri kullandığını ve environment override'ın audience validation'ı kaldırmadığını da kontrol etmelidir.
Password authentication için kontrol listesi
PasswordEncoderhangi bean üzerinden geliyor?NoOpPasswordEncoderveya{noop}legacy kayıtlar kabul ediliyor mu?- Password hash migration stratejisi var mı?
- Work factor güncel risk ve performansa göre ölçülmüş mü?
- Login ve reset flow'unda rate limit var mı?
- Account lockout denial-of-service üretmeden tasarlanmış mı?
- Password reset token'ı tek kullanımlık ve süreli mi?
- Reset sonrası mevcut session ve refresh token'lar iptal ediliyor mu?
- User enumeration response, timing veya log üzerinden oluşuyor mu?
- Disabled, locked ve expired account state doğru değerlendiriliyor mu?
DelegatingPasswordEncoder, farklı encoding formatlarının kontrollü migration'ı için kullanılabilir. Fakat legacy {noop} kayıtların sistemde kabul edilmesi veya custom fallback encoder'ın bilinmeyen formatı sessizce geçerli sayması ayrıca incelenmelidir.
Authorization review: Asıl inceleme controller'dan sonra başlar
Spring projesinde authentication çoğu zaman merkezi configuration içinde görünür. Kritik authorization problemleri ise controller, service, repository, cache ve message flow arasına dağılır.
Şu üç soru ayrı ayrı cevaplanmalıdır:
- 1Kullanıcı bu endpoint'e erişebilir mi?
- 2Kullanıcı bu action'ı gerçekleştirebilir mi?
- 3Kullanıcı bu specific object veya tenant üzerinde bu action'ı gerçekleştirebilir mi?
authenticated() yalnızca ilk sorunun bir bölümünü cevaplar.
Riskli örnek: ID doğrudan repository'ye gidiyor
@RestController
@RequestMapping("/api/orders")
class OrderController {
private final OrderRepository orderRepository;
private final OrderMapper orderMapper;
OrderController(OrderRepository orderRepository, OrderMapper orderMapper) {
this.orderRepository = orderRepository;
this.orderMapper = orderMapper;
}
@GetMapping("/{orderId}")
OrderResponse getOrder(@PathVariable UUID orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(OrderNotFoundException::new);
return orderMapper.toResponse(order);
}
}Request authenticated olabilir. Ancak orderId ile authenticated principal arasında hiçbir ownership veya tenant ilişkisi kurulmamıştır.
Daha güçlü model: Tenant-scoped query service katmanında
@Service
class OrderQueryService {
private final OrderRepository orderRepository;
private final CurrentTenant currentTenant;
OrderQueryService(
OrderRepository orderRepository,
CurrentTenant currentTenant
) {
this.orderRepository = orderRepository;
this.currentTenant = currentTenant;
}
@PreAuthorize("hasAuthority('order:read')")
@Transactional(readOnly = true)
public OrderResponse getOrder(UUID orderId) {
UUID tenantId = currentTenant.requiredTenantId();
return orderRepository.findByIdAndTenantId(orderId, tenantId)
.map(OrderResponse::from)
.orElseThrow(OrderNotFoundException::new);
}
}
interface OrderRepository extends JpaRepository<Order, UUID> {
Optional<Order> findByIdAndTenantId(UUID id, UUID tenantId);
}Bu örnekte tenantId request parameter'dan alınmaz. Signed ve doğrulanmış identity context üzerinden üretilir. Query de object ID ile tenant boundary'yi aynı database operation içinde uygular.
Bu pattern her uygulama için tek doğru çözüm değildir. ABAC policy engine, row-level security veya merkezi AuthorizationManager kullanılabilir. Esas şart, object ve tenant kararının görünür, test edilebilir ve bütün entry point'lerde tutarlı olmasıdır.
IDOR ve object-level authorization hatalarının neden tekrarlandığını IDOR açığı neden hâlâ bu kadar yaygın? yazımızda teknik örneklerle açıklıyoruz.
Method security gerçekten etkin mi?
Spring Boot Starter Security, method-level authorization'ı otomatik olarak etkinleştirmez. @PreAuthorize kullanımlarının varlığı tek başına kontrolün çalıştığını kanıtlamaz.
Reviewer şu noktaları doğrulamalıdır:
@EnableMethodSecurityveya reactive karşılığı etkin mi?- İlgili configuration class aktif profile içinde mi?
- Annotation Spring-managed bean üzerinde mi?
- Method çağrısı proxy üzerinden mi geçiyor?
- Method
privateveya proxy'nin intercept edemediği bir yapıda mı? - Aynı bean içindeki self-invocation kontrolü atlıyor mu?
- Annotation interface, superclass veya implementation üzerinde nerede?
- Compile sırasında method parameter isimleri SpEL için erişilebilir mi?
- Custom
MethodSecurityExpressionHandlervarsayılan davranışı değiştiriyor mu? @PostAuthorizewrite operation sonrasında mı çalışıyor?
Self-invocation örneği
@Service
class ReportService {
public Report download(UUID reportId) {
return loadSensitiveReport(reportId);
}
@PreAuthorize("@reportAuthorization.canRead(#reportId, authentication)")
public Report loadSensitiveReport(UUID reportId) {
return loadFromStorage(reportId);
}
}download() içinden loadSensitiveReport() çağrısı aynı object üzerindeki doğrudan Java çağrısıdır. Proxy üzerinden geçmediği için method security advice beklenen biçimde çalışmayabilir.
Daha güvenli seçenekler şunlardır:
- Authorization'ı public entry method üzerinde uygulamak
- Güvenlik sınırını ayrı Spring bean'e taşımak
- Programmatic
AuthorizationManagerkullanmak - Architecture test ile secured service boundary'yi zorunlu kılmak
Annotation görmek yerine interceptor'ın gerçekten devreye girdiği negative test ile kanıt üretilmelidir.
@PostAuthorize write işlemi için geç kalabilir
@PostAuthorize, method çalıştıktan sonra karar verir. Read operation sonucunu filtrelemek için yararlı olabilir. Fakat database write tamamlandıktan sonra erişimi reddetmek, yetkisiz değişikliğin hiç gerçekleşmediğini garanti etmez.
State-changing operation için authorization mümkün olduğunca mutation öncesinde yapılmalıdır. Transaction ve advisor ordering ayrıca incelenmelidir.
Role kontrolünden object ve business rule kontrolüne geçin
Şu kontrol kaba bir role gate sağlar:
@PreAuthorize("hasRole('ADMIN')")Fakat gerçek sistemlerde authorization çoğu zaman şu bileşenlerden oluşur:
subject
+ action
+ object
+ tenant
+ object state
+ transaction limit
+ channel
+ time
+ delegated authorityÖrneğin APPROVER role'üne sahip olmak her payment'ı onaylama yetkisi vermeyebilir. Kullanıcının kendi başlattığı payment'ı onaylamaması, tutarın limit altında olması ve aynı tenant içinde bulunması gerekebilir.
Bu nedenle reviewer şu anti-pattern'leri aramalıdır:
- Yalnızca role kontrolü yapılıp object ownership'in unutulması
- Tenant ID'nin request'ten alınması
- Admin bypass'ın geniş authority ile kontrol edilmesi
support=truegibi request flag'i ile privilege açılması- Bulk endpoint'te tekil endpoint policy'sinin uygulanmaması
- Export ve search endpoint'lerinin object policy'yi atlaması
- Indirect reference üzerinden aynı veriye ikinci erişim yolu
- Workflow state değişmeden önce değil, sonra authorization yapılması
- Delegated access'in expiry ve scope olmadan kalıcılaşması
- Cache'ten dönen object üzerinde policy'nin yeniden uygulanmaması
Data binding ve mass assignment incelemesi
Spring MVC'nin data binding özellikleri geliştirici deneyimini kolaylaştırır. Güvenlik açısından temel soru, request'ten gelen hangi field'ların hangi object'e yazılabildiğidir.
Riskli pattern: JPA entity'yi request body olarak almak
@PatchMapping("/users/{id}")
User updateUser(
@PathVariable UUID id,
@RequestBody User user
) {
user.setId(id);
return userRepository.save(user);
}User entity'sinde role, tenantId, enabled, creditLimit veya passwordHash gibi field'lar bulunuyorsa client beklenmeyen property'leri bind edebilir.
@Valid bu problemi kendiliğinden çözmez. Validation, kabul edilen object graph içindeki constraint'leri kontrol eder. Hangi field'ı client'ın yazmaya yetkili olduğunu belirlemez.
Daha güçlü pattern: Dar input DTO ve explicit mapping
public record ChangeDisplayNameRequest(
@NotBlank
@Size(max = 80)
String displayName
) {
}
@RestController
@RequestMapping("/api/profile")
class ProfileController {
private final ProfileService profileService;
ProfileController(ProfileService profileService) {
this.profileService = profileService;
}
@PatchMapping("/display-name")
ResponseEntity<Void> changeDisplayName(
@Valid @RequestBody ChangeDisplayNameRequest request
) {
profileService.changeDisplayName(request.displayName());
return ResponseEntity.noContent().build();
}
}Dar DTO tek başına bütün authorization sorunlarını çözmez. Fakat web input ile persistence entity arasındaki writable surface'i küçültür.
Property binding kullanılıyorsa allowlist tercih edin
Spring Framework dokümantasyonu, property binding gereken durumlarda allowedFields kullanılmasını önerir. Denylist yeni eklenen field'ların unutulmasına açıktır.
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("displayName", "locale");
}Reviewer yalnızca @RequestBody değil, aşağıdaki binding yollarını da aramalıdır:
@ModelAttribute- Query parameter ile POJO binding
BeanUtils.copyProperties- ModelMapper veya MapStruct custom mapping
- Jackson
ObjectMapper.convertValue - JSON Merge Patch
- GraphQL input object
- Form-data binding
- Admin import dosyaları
Validation ile security rule aynı şey değildir
@Size, @Pattern, @Min veya custom Bean Validation annotation'ları input constraint sağlar. Fakat şu güvenlik sorularını ayrı ele almak gerekir:
- Kullanıcı bu field'ı göndermeye yetkili mi?
- Değer bu object state'i için geçerli mi?
- İki field birlikte değerlendirildiğinde business invariant korunuyor mu?
- Validation bütün entry point'lerde çalışıyor mu?
- Service method doğrudan çağrıldığında aynı kural uygulanıyor mu?
- Normalization validation'dan önce mi sonra mı?
- Unicode ve locale farkı policy'yi değiştiriyor mu?
- Server-side generated değer client tarafından override edilebiliyor mu?
Validation controller'da, security invariant ise domain veya service sınırında uygulanabilir. Böylece aynı business operation message consumer üzerinden çağrıldığında da korunur.
Persistence ve query incelemesi: JPA kullanmak otomatik güvenlik sağlamaz
Spring Data JPA derived query ve parameter binding, value injection riskini önemli ölçüde azaltır. Ancak uygulama query text, sort expression, table, column veya fragment'i kullanıcı girdisiyle birleştiriyorsa risk yeniden oluşur.
Riskli dynamic query
String jpql = "select o from Order o where o.tenantId = :tenantId order by "
+ requestedSort;
TypedQuery<Order> query = entityManager.createQuery(jpql, Order.class);
query.setParameter("tenantId", tenantId);tenantId bind edilmiştir. Fakat requestedSort query structure içine doğrudan eklenmiştir.
Allowlist ile sort mapping
private static final Map<String, String> ALLOWED_SORTS = Map.of(
"createdAt", "o.createdAt",
"total", "o.totalAmount",
"status", "o.status"
);
private String resolveSort(String requestedSort) {
String sort = ALLOWED_SORTS.get(requestedSort);
if (sort == null) {
throw new InvalidSortFieldException();
}
return sort;
}Tercihen query construction API veya validated Sort kullanılmalıdır. JpaSort.unsafe() kullanımları özellikle incelenmelidir. API adındaki unsafe ifadesi, function veya expression içeren order fragment'lerine izin verildiğini açıkça belirtir.
Repository review sırasında aranacak pattern'ler
rg -n 'createNativeQuery|createQuery|JdbcTemplate|NamedParameterJdbcTemplate' src
rg -n '@Query|@NativeQuery|nativeQuery\\s*=\\s*true' src
rg -n 'JpaSort\\.unsafe|Sort\\.by|PageRequest\\.of' src
rg -n 'StringBuilder|String\\.format|formatted\\(' src/mainHer string concatenation SQL injection değildir. Reviewer, user-controlled source ile query structure sink'i arasındaki data flow'u kanıtlamalıdır.
Tenant filter tek başına güvenilir mi?
Hibernate filter, interceptor veya repository base class tenant isolation için kullanılabilir. Fakat şu durumlar ayrıca araştırılmalıdır:
- Native query filter'ı atlıyor mu?
- Background job tenant context olmadan çalışıyor mu?
- Admin repository aynı base class'ı bypass ediyor mu?
- Cache key tenant'ı içeriyor mu?
- Async task'ta tenant context taşınıyor mu?
- Direct EntityManager kullanımı ortak kontrolü geçiyor mu?
- Migration veya report module farklı datasource kullanıyor mu?
Bir tenant filter'ın bulunması, bütün query path'lerinin scoped olduğunu kanıtlamaz.
SpEL ve dynamic expression kullanımlarını ayırın
Spring Expression Language birçok framework özelliğinde developer tarafından yazılan sabit expression'lar için kullanılır. @PreAuthorize("hasAuthority('order:read')") kullanımı tek başına expression injection değildir.
Risk, user-controlled verinin expression metninin kendisine dönüştürülmesiyle başlar:
Expression expression = parser.parseExpression(request.getRule());
Object result = expression.getValue(context);Reviewer şu kullanımları aramalıdır:
rg -n 'SpelExpressionParser|parseExpression|StandardEvaluationContext' src
rg -n 'setVariable|registerFunction|setBeanResolver|setTypeLocator' srcİncelenecek noktalar:
- Expression text kullanıcı veya tenant administrator tarafından değiştirilebiliyor mu?
- Evaluation context method, type veya bean erişimini açıyor mu?
SimpleEvaluationContextyeterli bir alternatif olabilir mi?- Expression önceden tanımlı rule ID üzerinden seçilebilir mi?
- Expression sonucu authorization veya data access kararında mı kullanılıyor?
- Stored expression daha sonra higher-privilege context içinde mi çalışıyor?
String blacklist ile SpEL güvenli hale getirilmeye çalışılmamalıdır. Kullanıcının serbest expression yazmasına ihtiyaç yoksa sabit rule set veya typed policy model tercih edilmelidir.
Outbound request ve SSRF incelemesi
Spring uygulamalarında outbound HTTP çağrıları şu client'lar üzerinden görülebilir:
RestClientWebClientRestTemplate- OpenFeign
- HTTP Service Client
- Apache HttpClient
- JDK
HttpClient
Reviewer için kritik data flow şudur:
request veya stored user data
→ URL veya host oluşturma
→ DNS resolution
→ redirect
→ outbound connectionRiskli örnek:
Mono<String> response = webClient.get()
.uri(request.callbackUrl())
.retrieve()
.bodyToMono(String.class);En güçlü tasarım, client'ın arbitrary URL vermesine izin vermemektir. Kullanıcı yalnızca önceden tanımlı destination ID veya business identifier seçer. Host ve scheme server-side configuration'dan gelir.
@Component
class BillingClient {
private final RestClient restClient;
BillingClient(
RestClient.Builder builder,
@Value("${services.billing.base-url}") String baseUrl
) {
this.restClient = builder
.baseUrl(baseUrl)
.build();
}
InvoiceResponse getInvoice(UUID invoiceId) {
return restClient.get()
.uri("/internal/invoices/{invoiceId}", invoiceId)
.retrieve()
.body(InvoiceResponse.class);
}
}Bu örnekte remote origin request'ten gelmez. Production egress policy, destination TLS validation, timeout, response size ve redirect davranışı yine ayrıca yapılandırılmalıdır.
Arbitrary URL iş gereksinimi gerçekten varsa scheme, hostname, port, DNS sonucu, private ve link-local range, IPv4 ve IPv6 normalization, redirect chain, proxy davranışı ve egress kontrolü birlikte ele alınmalıdır. Yalnızca string allowlist yeterli değildir.
Bu konunun bypass yüzeyini SSRF'yi kapatmak için yalnızca allowlist yeterli mi? yazımızda ayrıntılı olarak inceliyoruz.
File upload, path ve archive işlemlerini izleyin
Java Spring projelerinde file flow yalnızca MultipartFile kabul eden controller'dan ibaret değildir. Aşağıdaki noktalar birlikte izlenmelidir:
- Upload request
- Original filename
- MIME type
- Content detection
- Temporary directory
- Antivirus veya sandbox
- Image veya document parser
- Archive extraction
- Object storage key
- Download response
- Cleanup job
- Public URL üretimi
getOriginalFilename() güvenilir değildir
Client tarafından sağlanan filename path, control character veya beklenmeyen Unicode içerebilir. Filename doğrudan filesystem path, object storage key veya response header haline getirilmemelidir.
Aranabilecek pattern'ler:
rg -n 'MultipartFile|getOriginalFilename|transferTo' src
rg -n 'Paths\\.get|Path\\.of|resolve\\(|normalize\\(|toRealPath' src
rg -n 'ZipInputStream|ZipEntry|TarArchiveInputStream' src
rg -n 'Files\\.copy|Files\\.write|Files\\.newOutputStream' srcnormalize() tek başına symlink problemini çözmez
Şu kontrol lexical path traversal'ı azaltır:
Path candidate = baseDirectory.resolve(userValue).normalize();
if (!candidate.startsWith(baseDirectory)) {
throw new InvalidPathException(userValue, "Base directory dışına çıkıyor");
}Fakat writable directory içinde symlink bulunabiliyorsa lexical startsWith kontrolü gerçek filesystem target'ını garanti etmez. toRealPath, NOFOLLOW_LINKS, storage ownership ve race condition birlikte değerlendirilmelidir.
Daha güçlü architecture seçenekleri:
- Server-generated random object key
- User filename'i yalnızca metadata olarak tutmak
- Upload alanını executable veya web root dışında konumlandırmak
- Parser'ı düşük privilege sandbox içinde çalıştırmak
- Archive entry'lerini ayrı ayrı canonical boundary kontrolünden geçirmek
- File type'ı extension ve request
Content-Typeyerine content üzerinden doğrulamak - Size, compression ratio, entry count ve processing timeout sınırı koymak
Serialization ve parser yüzeyi
Spring application çoğu zaman Jackson ile JSON işler. Fakat repository içinde şu parser ve serialization yolları da bulunabilir:
- Java native serialization
- Jackson polymorphic deserialization
- XML parser
- YAML parser
- Object mapping
- Message broker serializer
- Cache serialization
- Session serialization
- Template engine
- Expression engine
Aranabilecek başlangıç pattern'leri:
rg -n 'ObjectInputStream|readObject|readUnshared' src
rg -n 'ObjectMapper|activateDefaultTyping|enableDefaultTyping|@JsonTypeInfo' src
rg -n 'DocumentBuilderFactory|SAXParserFactory|XMLInputFactory|TransformerFactory' src
rg -n 'Yaml|SnakeYAML|loadAs|load\\(' srcJackson için sorulacak sorular
- Global
ObjectMapperkim tarafından oluşturuluyor? - Spring Boot defaults custom bean nedeniyle tamamen değişiyor mu?
- Polymorphic typing etkin mi?
- Client type discriminator gönderebiliyor mu?
- İzin verilen subtype seti dar mı?
- Sensitive field'lar deserialize edilebiliyor mu?
- Unknown property davranışı security contract'i etkiliyor mu?
- Serialization view hassas alanı gerçekten gizliyor mu?
- Aynı DTO message broker ve public HTTP için ortak mı?
Güvensiz deserialization yalnızca “eski Java readObject bulundu” şeklinde değerlendirilmemelidir. Type control, reachable gadget, classpath, trust boundary ve data origin birlikte kanıtlanmalıdır.
XML parser review
XXE riskinde yalnızca DOCTYPE kapatma satırı aranmaz. Parser factory'nin bütün creation path'leri, external entity ve external DTD erişimi, XInclude, schema resolution ve transformer davranışı incelenir.
Bir merkezi secure factory bulunması değerlidir. Fakat başka bir module doğrudan yeni parser oluşturuyorsa ortak kontrol bypass edilebilir.
Server-side rendering ve XSS review
Spring MVC ile Thymeleaf, JSP veya başka template engine kullanılıyorsa output context önemlidir.
Özellikle şu pattern'ler incelenmelidir:
- Thymeleaf
th:utext - JSP scriptlet veya raw output
- User-controlled template name
- User-controlled view name
- Dynamic JavaScript block
- HTML email template
- Markdown-to-HTML conversion
- Rich text sanitizer
- File preview
- Response header'a user input eklenmesi
th:text escaped output üretirken th:utext unescaped HTML render eder. Bu kullanım her zaman vulnerability değildir. Input'un trusted ve güvenli biçimde sanitize edildiği kanıtlanmalıdır.
Security header'lar defense-in-depth sağlar. Context-aware output encoding ve güvenilir sanitization yerine geçmez.
CSRF, CORS ve session configuration birlikte incelenmeli
Bu üç konu ayrı config bloklarında görünse de browser trust modelinde birbirini etkiler.
csrf.disable() her zaman vulnerability midir?
Hayır. Fakat gerekçesiz biçimde güvenli de değildir.
CSRF riskini belirlemek için browser'ın credential'ı request'e otomatik ekleyip eklemediğine bakılır.
CSRF protection'ın kapatılması şu durumda makul olabilir:
- API yalnızca non-browser veya açıkça tasarlanmış client'lar tarafından kullanılıyor
- Authentication yalnızca
Authorization: Bearerheader'ı ile geliyor - Token browser tarafından ambient credential olarak otomatik eklenmiyor
- Cookie tabanlı fallback bulunmuyor
- State-changing endpoint'ler başka otomatik credential kabul etmiyor
Şu durumlarda risk yeniden değerlendirilmelidir:
- Session cookie
- Remember-me cookie
- Cookie içinde JWT
- Browser Basic authentication
- Client certificate
- Hybrid web ve API endpoint'leri
- Aynı chain içinde form login ve Resource Server
Reviewer config satırını değil, credential transport modelini incelemelidir.
CORS authorization değildir
CORS browser'ın response'u başka origin'deki JavaScript'e açıp açmayacağını kontrol eder. Backend endpoint'ine kimin yetkili olduğunu belirlemez.
Kontrol listesi:
- Allowed origin exact mi?
- Wildcard ve credentials birlikte kullanılıyor mu?
- Origin pattern beklenenden geniş mi?
- Tenant subdomain wildcard'ı takeover riski taşıyor mu?
- Allowed method ve header gereğinden geniş mi?
- Preflight doğru filter order içinde mi?
- CORS config MVC ve Security tarafında çelişiyor mu?
- Actuator için ayrı CORS config var mı?
Spring Security dokümantasyonu, preflight request cookie taşımadığı için CORS'un security chain'den önce doğru biçimde işlenmesi gerektiğini belirtir.
Stateless config gerçekten stateless mi?
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);Bu configuration server'ın security context için session kullanmamasını hedefler. Ancak aşağıdakileri otomatik olarak garanti etmez:
- Uygulamanın başka kodu session oluşturmuyor
- Load balancer cookie üretmiyor
- Token server-side revoke edilebiliyor
- Refresh token güvenli saklanıyor
- Logout token'ı gerçekten geçersizleştiriyor
- Browser token storage güvenli
- CSRF riski yok
Session policy, bütün authentication lifecycle'ın yalnızca bir parçasıdır.
Error handling ve fail-open path'leri
Security review yalnızca başarılı request flow'unu izlerse kritik bypass'ları kaçırabilir.
Şunlar araştırılmalıdır:
AuthenticationExceptionnasıl ele alınıyor?AccessDeniedExceptiongeneric exception handler tarafından yutuluyor mu?- Policy service timeout olduğunda allow mu deny mı?
- Remote identity provider ulaşılamadığında cached privilege ne kadar geçerli?
- Feature flag service hata verdiğinde security feature açılıyor mu kapanıyor mu?
- Exception sonrası tenant filter devre dışı kalıyor mu?
- Retry aynı state-changing operation'ı tekrar ediyor mu?
- Fallback method daha az kontrolle veri döndürüyor mu?
- Error response stack trace, query veya secret içeriyor mu?
Riskli bir fallback örneği:
try {
authorizationClient.check(permission);
return sensitiveOperation();
} catch (RuntimeException ex) {
log.warn("Authorization service unavailable", ex);
return sensitiveOperation();
}Burada dependency failure, authorization bypass'a dönüşür.
Security control ulaşılamadığında default karar açıkça tanımlanmalıdır. Her fail-closed kararı da availability açısından kör uygulanmamalıdır. Kritik nokta, fallback'in business owner ve security owner tarafından bilinmesi, sınırlanması ve test edilmesidir.
Transaction, concurrency ve state transition review
Business logic açığı çoğu zaman tek request içinde görünmez.
Örnek:
1. Payment status kontrol edilir
2. Status PENDING ise balance düşürülür
3. Payment APPROVED yapılırİki concurrent request aynı PENDING state'i okuyabiliyorsa işlem iki kez gerçekleşebilir.
Reviewer şu alanları inceler:
- Check-then-act pattern
@Transactionalboundary- Isolation level
- Optimistic locking ve
@Version - Pessimistic lock kullanımı
- Unique constraint
- Idempotency key
- Retry policy
- Message delivery semantics
- Outbox ve saga state'i
- Compensation action
- State machine transition
- Approval separation
@Transactional annotation'ı tek başına race condition çözmez. Transaction isolation ve database constraint gerçek invariant'a göre tasarlanmalıdır.
Transaction annotation'ında da proxy sınırı vardır
Method security gibi Spring transaction yönetimi de çoğu kullanımda proxy tabanlıdır. Aynı class içindeki self-invocation, beklenen transaction boundary'nin oluşmamasına neden olabilir.
Reviewer security ve transaction annotation'larını ayrı ayrı görmek yerine gerçek invocation path üzerinde birlikte değerlendirmelidir.
Cache review: Authorization'dan önce mi sonra mı?
Cache, multi-tenant uygulamalarda görünmeyen data isolation problemi üretebilir.
Riskli key:
@Cacheable(cacheNames = "orders", key = "#orderId")
public OrderResponse getOrder(UUID orderId) {
return orderRepository.findResponseById(orderId)
.orElseThrow(OrderNotFoundException::new);
}Eğer aynı orderId farklı namespace veya tenant context'inde farklı anlam taşıyorsa cache key yetersizdir. Daha geniş risk, ilk authorized request'in sonucunun ikinci kullanıcıya policy tekrar çalışmadan dönmesidir.
İncelenecek noktalar:
- Cache key tenant ve security context'i içeriyor mu?
- Method security advisor ile cache advisor sırası nedir?
- Cache hit olduğunda authorization yine çalışıyor mu?
- Negative authorization sonucu cache'leniyor mu?
- Role değişikliği sonrası cache invalidation var mı?
- Sensitive response shared cache'e yazılıyor mu?
- HTTP cache header'ları private data için uygun mu?
- Redis key naming tenant collision'a açık mı?
Cache key içine doğrudan bütün principal object'ini koymak da iyi çözüm değildir. Stable ve gerekli security boundary identifier'ları seçilmelidir.
Async, scheduler ve messaging flow'larında security context
HTTP request içinde bulunan SecurityContext, yeni thread veya message boundary'ye kendiliğinden güvenli bir authorization contract olarak taşınmayabilir.
Reviewer şu soruları sormalıdır:
@Asyncmethod hangi identity ile çalışıyor?- Executor security context'i bilinçli taşıyor mu?
- Context taşınması gerçekten gerekli mi?
- Queue message içinde subject, tenant ve authority claim'leri nasıl doğrulanıyor?
- Message producer'ın identity'si consumer tarafından güveniliyor mu?
- Replay engelleniyor mu?
- Message schema version authorization'ı etkiliyor mu?
- Scheduled job system authority ile mi tenant authority ile mi çalışıyor?
- Batch job scope'u nasıl sınırlandırılıyor?
- Dead-letter replay aynı operation'ı tekrar tetikliyor mu?
Bir HTTP controller'ın authorization kontrolü, queue consumer için koruma sağlamaz. Service boundary'deki invariant veya consumer'a özgü policy ayrıca bulunmalıdır.
WebFlux için ThreadLocal varsayımı yapmayın
Reactive application'da security context Reactor Context içinde taşınır. ThreadLocal üzerinden principal veya tenant alan custom helper'lar scheduler değişiminde yanlış veya boş context görebilir.
WebFlux review sırasında:
ReactiveSecurityContextHolderServerHttpSecuritySecurityWebFilterChain@EnableReactiveMethodSecurity- Reactive authorization manager
- Context propagation
.subscribe()ile kontrolsüz detached flow- Blocking bridge ve thread switch
ayrıca incelenmelidir.
Actuator ve management surface review
Actuator endpoint'leri application hakkında güçlü operasyonel bilgi ve bazı durumlarda state-changing operation sunabilir.
Özellikle şu config'ler aranmalıdır:
rg -n 'management\\.endpoints|management\\.endpoint|management\\.server' .
rg -n 'EndpointRequest|@Endpoint|@WebEndpoint|@ControllerEndpoint|@RestControllerEndpoint' srcRiskli configuration:
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: alwaysBu config tek başına erişimin public olduğunu kanıtlamaz. Ancak bütün endpoint'lerin web exposure'a açıldığını ve authorization'ın ayrıca kanıtlanması gerektiğini gösterir.
Reviewer şu soruları cevaplamalıdır:
- Hangi endpoint'ler enabled?
- Hangileri HTTP veya JMX üzerinden exposed?
- Management port ve bind address nedir?
- Network policy gerçekten erişimi sınırlandırıyor mu?
- Custom
SecurityFilterChainactuator auto-configuration'ı değiştirdi mi? /healthdetail ve component bilgisi kime gösteriliyor?env,configprops,mappings,heapdump,threaddump,logfileveloggerserişimi nasıl sınırlandırılıyor?- Custom
@WriteOperationauthorization gerektiriyor mu? - Management endpoint CORS policy'si ayrı mı?
- Sensitive value sanitization'a kör güveniliyor mu?
Spring Boot'ta custom SecurityFilterChain tanımlandığında application security configuration'ının Actuator erişimini de açık biçimde ele alması gerekir.
Secret, configuration ve logging review
Spring uygulamalarında güvenlik kararı source code dışındaki config ile değişebilir.
İncelenecek dosya ve kaynaklar:
application.ymlapplication-*.ymlbootstrap.yml- Environment variable
- Config server
- Kubernetes Secret ve ConfigMap
- Helm values
- Dockerfile
- CI/CD variable
- Test fixture
- Local development config
- Logging config
Secret aramasında yalnızca password= kullanmayın
Aranabilecek örnekler:
rg -n -i 'password|passwd|secret|api[_-]?key|client[_-]?secret|private[_-]?key|token' .
rg -n 'BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY' .
rg -n 'Authorization|Cookie|Set-Cookie|X-Api-Key' srcBu aramalar false positive üretebilir. Finding için değerin gerçek secret olup olmadığı, geçerli environment'a ait olup olmadığı ve history içinde bulunup bulunmadığı doğrulanmalıdır.
Log üzerinden data exposure
Şu pattern'ler önemlidir:
- Request body'nin tamamını loglamak
- Authorization header loglamak
- JWT'yi exception message'e koymak
- JPA entity
toString()ile sensitive field yazmak - Password reset token'ını loglamak
- Payment veya health verisini masking olmadan yazmak
- Multi-line input ile log forging
- Central logging erişimini application erişimiyle eşdeğer sanmak
Security logging gerekli olsa da sensitive data minimization ile birlikte tasarlanmalıdır. “Daha çok log” her zaman “daha güvenli” değildir.
Custom filter incelemesinde neye bakılır?
Spring Security dışına eklenen custom authentication veya tenant filter'ları yüksek riskli review alanıdır.
class ApiKeyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
filterChain.doFilter(request, response);
}
}Reviewer şu soruları inceler:
- Filter Spring Security chain içinde mi, Servlet container'a ayrı mı kayıtlı?
- Aynı filter iki kez çalışıyor mu?
- Bütün dispatcher type'ları için davranış tutarlı mı?
- Error ve async dispatch'te ne oluyor?
- Credential yoksa chain devam ediyor mu?
- Geçersiz credential ile anonymous credential ayrıştırılıyor mu?
- Authentication object fully authenticated olarak mı işaretleniyor?
- Authority listesi request header'dan mı geliyor?
- SecurityContext request sonunda temizleniyor mu?
- Exception doğru entry point'e mi gidiyor?
- Filter order CSRF, CORS ve authorization ile uyumlu mu?
- Path exclusion listesi normalization farkına açık mı?
Custom filter'da filterChain.doFilter() çağrısının hem error path hem success path içinde yanlışlıkla iki kez yapılması da kontrol edilmelidir.
Hızlı triage için arama listesi
Aşağıdaki aramalar review'in yerini tutmaz. İlk risk haritasını hızlandırır:
rg -n 'permitAll|denyAll|authenticated|hasRole|hasAuthority' src
rg -n 'csrf\\(.*disable|csrf\\.disable|cors\\(.*disable' src
rg -n 'anyRequest|securityMatcher|requestMatchers|@Order' src
rg -n '@PreAuthorize|@PostAuthorize|@Secured|@RolesAllowed|@PermitAll' src
rg -n 'getHeader|getParameter|getCookies|getOriginalFilename' src
rg -n 'createNativeQuery|createQuery|JdbcTemplate|JpaSort\\.unsafe' src
rg -n 'RestClient|WebClient|RestTemplate|FeignClient|HttpClient' src
rg -n 'ObjectInputStream|parseExpression|ScriptEngine|Runtime\\.getRuntime|ProcessBuilder' src
rg -n 'Files\\.|Path\\.of|Paths\\.get|ZipInputStream|ZipEntry' src
rg -n 'th:utext|setHeader|addHeader|Content-Disposition' .
rg -n 'management\\.endpoints\\.web\\.exposure|show-details|heapdump|loggers' .
rg -n 'catch \\(Exception|catch \\(RuntimeException|onErrorResume|fallbackMethod' srcReviewer her eşleşme için şu üç soruyu sormalıdır:
- 1Veri veya kararın kaynağı güvenilir mi?
- 2Hangi security boundary geçiliyor?
- 3Runtime'da bu path gerçekten reachable mı?
Bu sorular cevaplanmadan yalnızca code pattern üzerinden severity verilmemelidir.
İlk 90 dakikada uygulanabilecek repository-first akış
Büyük bir Spring repository'sinde ilk inceleme oturumu şu sırayla yürütülebilir:
| Süre | İnceleme | Beklenen çıktı |
|---|---|---|
| 0–15 dakika | Module, build, wrapper ve dependency management | Build ve module haritası |
| 15–30 dakika | application-*, profile, deployment ve management config | Runtime configuration haritası |
| 30–45 dakika | MVC, WebFlux, messaging, scheduler ve Actuator entry point'leri | Attack surface inventory |
| 45–60 dakika | Bütün filter chain, method security ve authentication bean'leri | Security control map |
| 60–75 dakika | En kritik business flow'da controller-service-repository izi | İlk end-to-end authorization trace |
| 75–90 dakika | Negative test, limitation ve review backlog | Kanıt ve öncelik listesi |
İlk 90 dakikanın amacı vulnerability raporunu bitirmek değildir. Yanlış module'e veya inactive profile'a saatler harcamadan güvenlik mimarisini görünür hale getirmektir.
Risk bazlı dosya önceliği nasıl belirlenir?
Her dosya eşit risk taşımaz. Reviewer aşağıdaki sinyalleri birlikte kullanabilir:
- Public veya partner-facing entry point
- Authentication ve authorization kararı
- Multi-tenant data access
- Payment, approval veya transfer state'i
- File ve parser işlemi
- Outbound network access
- Secret veya cryptographic key kullanımı
- Admin, support veya impersonation özelliği
- Dynamic query veya expression
- Custom security framework
- Legacy module
- Son dönemde yoğun change alanı
- Önceki incident veya pentest bulgusu
- Düşük test coverage
- Çok sayıda consumer tarafından kullanılan shared component
Bu önceliklendirme “diğer dosyalar güvenlidir” anlamına gelmez. Manual review eforunu kritik attack path'lere yönlendirir. Otomatik SAST ve SCA geniş pattern coverage için paralel kullanılabilir.
Negative security test olmadan code review tamamlanmış sayılmamalı
Code review finding'i mümkünse test ile kalıcılaştırılmalıdır.
Örnek bir object-level authorization integration testi:
@SpringBootTest
@AutoConfigureMockMvc
class OrderAuthorizationIntegrationTest {
@Autowired
MockMvc mockMvc;
@Test
@WithMockUser(
username = "alice",
authorities = {"order:read"}
)
void userCannotReadAnotherTenantsOrder() throws Exception {
UUID anotherTenantOrderId =
UUID.fromString("2afc63d9-8c80-4a32-a442-52df82e9026c");
mockMvc.perform(get("/api/orders/{id}", anotherTenantOrderId))
.andExpect(status().isNotFound());
}
}Bu test örneğinde uygulamanın cross-tenant object'i 404 ile gizlemeyi seçtiği varsayılmıştır. Bazı sistemler 403 kullanabilir. Güvenlik kriteri status code tercihi değil, verinin ve side effect'in yetkisiz kullanıcıya açılmamasıdır.
Test fixture'ının alice kullanıcısını bir tenant'a, hedef order kaydını farklı bir tenant'a bağladığı varsayılmıştır. Gerçek projede bu ilişki test içinde açıkça kurulmalı ve assertion öncesinde doğrulanmalıdır.
@WithMockUser testinin sınırı
@WithMockUser, SecurityContext'i test için hazırlar. Gerçek JWT signature, issuer, audience ve authority mapping sürecini test etmez.
Bu nedenle test katmanları ayrılmalıdır:
- Method ve request authorization testi
- Gerçekçi JWT decoder veya test issuer ile authentication integration testi
- Cross-tenant ve cross-role negative test
- Production'a yakın end-to-end test
Tek bir test türü bütün security stack'i kanıtlamaz.
Review finding'i nasıl yazılmalı?
“Authorization eksik” başlığı geliştirici için yeterli değildir. Güçlü bir finding şu bileşenleri içermelidir:
- Security requirement
- Affected commit ve module
- Entry point
- Authentication precondition
- User role ve tenant
- Source code path
- Authorization decision'in eksik veya yanlış olduğu nokta
- Reachability kanıtı
- Abuse scenario
- Confidentiality, integrity ve availability etkisi
- Benzer pattern'in yayılımı
- Root cause
- Remediation seçeneği
- Regression test
- Runtime validation sonucu
- Limitation
Örnek özet:
finding:
title: Cross-tenant order read through unscoped repository lookup
severity: high
affected_commit: 9d2f4a7
entry_point: GET /api/orders/{orderId}
precondition:
authentication: valid customer JWT
authority: order:read
root_cause:
- order lookup uses globally unique ID
- tenant boundary is not included in repository query
- tenantId is not derived from trusted identity context
impact:
- authenticated customer can read another tenant's order metadata
systemic_search:
pattern: findById calls reachable from tenant-facing controllers
related_candidates: 6
remediation:
- derive tenant from verified identity context
- enforce tenant scope in repository query
- add cross-tenant negative tests
runtime_validation: confirmedBu kayıt yalnızca vulnerable line'ı değil, problemin güvenlik modelindeki yerini gösterir.
Java Spring güvenlik kod incelemesinde yaygın hatalar
Yalnızca SecurityConfig okumak
Filter chain önemli başlangıç noktasıdır. Fakat object-level authorization, business rule, repository scope ve alternate entry point'ler başka katmanlarda bulunur.
Annotation'ı çalışan kontrol sanmak
@PreAuthorize source code'da bulunabilir. Method security inactive, self-invocation mevcut veya bean Spring proxy dışında olabilir.
Bütün permitAll kullanımlarını finding yapmak
Login, public catalog, health probe veya static content bilinçli olarak public olabilir. Asıl soru endpoint'in public olmasının security requirement ile uyumlu olup olmadığıdır.
Bütün csrf.disable() kullanımlarını finding yapmak
Credential transport ve client modeli incelenmeden karar vermek false positive üretir. Aynı şekilde “API stateless” açıklamasını kanıtsız kabul etmek false negative üretir.
JPA varsa SQL injection olmaz demek
Native query, dynamic fragment, JpaSort.unsafe, custom repository ve JDBC path'leri ayrıca incelenmelidir.
@Valid varsa input güvenli demek
Validation authorization, output encoding, mass assignment veya business invariant yerine geçmez.
Sadece HTTP controller'ları taramak
Kafka, WebSocket, GraphQL, scheduler, batch, Actuator ve maintenance endpoint aynı service layer'a ulaşabilir.
Production profile'ı görmeden local config üzerinden karar vermek
Security bean, exposure, issuer, CORS ve management port profile'a göre değişebilir.
SCA listesini Secure Code Review raporu olarak sunmak
Dependency CVE listesi application logic, authorization ve custom data flow'u incelemez.
Finding'i line number ile sınırlamak
Line değişebilir. Root cause, invocation path, requirement ve regression test yoksa düzeltmenin sistemik olup olmadığı anlaşılamaz.
SAST bu incelemede nerede kullanılmalı?
SAST özellikle şu alanlarda reviewer'ı hızlandırabilir:
- Request input'tan SQL, command veya expression sink'ine data flow
- Path traversal adayları
- SSRF source-to-client flow
- Hardcoded secret
- Weak cryptographic API
- Unsafe deserialization
- Trust-all TLS configuration
- Log injection
- Response splitting
- Repeated authorization annotation eksikliği
- Custom rule ile tenant scope pattern'i
Fakat SAST'ın Spring model'i doğrulanmalıdır:
- Framework version destekleniyor mu?
- Annotation ve generated code çözümleniyor mu?
- Lombok sonrası semantic model doğru mu?
- Spring Data repository method'ları modelleniyor mu?
- WebFlux reactive flow izleniyor mu?
- Custom authentication wrapper source olarak tanınıyor mu?
- Custom sanitizer gerçekten güvenli mi?
- Multi-module call graph tamamlanıyor mu?
- Build başarılı mı?
CI/CD'ye SAST eklerken yapılan uygulama hatalarını CI/CD'ye SAST eklerken en sık yapılan 7 hata yazımızda ayrıca ele alıyoruz.
Review kapsamı nasıl yazılmalı?
“Java kod analizi” hizmet tanımı yeterli değildir.
Scope en az şunları açıklamalıdır:
- Repository ve module
- Commit veya tag
- Java ve Spring generation
- MVC, WebFlux, messaging ve batch kapsamı
- Configuration repository kapsamı
- Profile ve deployment environment
- Authentication modeli
- Role ve tenant sayısı
- Kritik business flow
- Source line veya module coverage yaklaşımı
- SAST, SCA ve secret scanning kullanımı
- Manual review derinliği
- Runtime validation olup olmadığı
- Rapor ve retest modeli
- Hariç tutulan generated code ve third-party artifact
İncelemenin nasıl tekliflendirilmesi gerektiği, satır sayısından daha geniş bir scoping çalışması gerektirir. Kapsamı belirsiz bir “tüm kod taranacaktır” cümlesi gerçek coverage'i açıklamaz.
İnceleme sırası için pratik karar ağacı
Build tekrarlanabiliyor mu?
├─ Hayır → Wrapper, repository, plugin ve missing artifact limitation'ını çöz
└─ Evet
└─ Runtime stack nedir?
├─ MVC → SecurityFilterChain ve Servlet entry point'leri
├─ WebFlux → SecurityWebFilterChain ve reactive context
└─ Hybrid → İki modeli ayrı ayrı haritala
└─ Authentication nerede?
├─ Session → CSRF, cookie ve session lifecycle
├─ JWT → issuer, audience, claim ve authority mapping
└─ Custom → filter, provider ve fail-open path
└─ Authorization nerede?
├─ Request-only → service ve alternate entry point gap'i ara
├─ Method-level → proxy, self-invocation ve object policy'yi doğrula
└─ External policy → timeout, cache ve fallback kararını doğrula
└─ En kritik data flow'u controller'dan sink'e izleBu akış checklist'in yerini almaz. Reviewer'ın önce framework gerçekliğini, sonra business riskini takip etmesini sağlar.
Sonuç: Spring güvenlik incelemesi dosya aramakla değil, kontrol akışını kurmakla başlar
Java Spring projelerinde güvenlik kod incelemesine başlamak için ilk refleks SecurityConfig içinde tehlikeli satır aramak olabilir. Bu hızlı bir triage sağlar, fakat kapsamlı güvence üretmez.
Profesyonel inceleme şu sırayı izler:
- 1Commit, build, dependency ve runtime artifact eşleştirilir
- 2MVC, WebFlux, messaging, batch ve Actuator entry point'leri çıkarılır
- 3Bütün
SecurityFilterChainve method security davranışı tek modelde birleştirilir - 4Authentication claim ve credential lifecycle doğrulanır
- 5Authorization controller'dan service, repository, cache ve alternate entry point'lere kadar izlenir
- 6Data binding, query, outbound request, file ve parser sink'leri incelenir
- 7CSRF, CORS, session, error ve fallback davranışı runtime modeliyle değerlendirilir
- 8Transaction, concurrency, async ve tenant context'i negative scenario'larla test edilir
- 9Finding root cause, reachability ve regression test ile raporlanır
Spring Security uygulamaya güçlü yapı taşları verir. Güvenli sonuç ise bu yapı taşlarının doğru chain'de, doğru profile'da, doğru object ve business rule üzerinde çalıştığının kanıtlanmasıyla ortaya çıkar.
Secnodex, Java ve Spring tabanlı uygulamalarda yalnızca SAST çıktısı veya dependency CVE listesi üretmez. Build graph, security configuration, authentication, authorization, multi-tenant data access, business flow, persistence, outbound connection ve runtime behavior birlikte incelenir. Bulgular affected code path, root cause, abuse scenario, uygulanabilir remediation ve regression kriterleriyle raporlanır.
Java Spring projeniz için risk bazlı Secure Code Review kapsamı oluşturmak üzere Secnodex ile iletişime geçebilirsiniz.
Kaynaklar
- Spring Security – Authorization
- Spring Security – Architecture
- Spring Security – Java Configuration
- Spring Security – Authorize HttpServletRequests
- Spring Security – Method Security
- Spring Security – OAuth 2.0 Resource Server JWT
- Spring Security – Cross Site Request Forgery
- Spring Security – CORS
- Spring Security – Password Storage
- Spring Framework – Validation, Data Binding and Type Conversion
- Spring Framework – Data Binding
- Spring Framework – Proxying Mechanisms
- Spring Data JPA – Query Methods
- Spring Boot – Actuator Endpoints
- Spring Security Advisories
- Spring Support Policy
- OWASP Application Security Verification Standard
- OWASP Code Review Guide
Sık sorulan sorular
Java Spring projesinde güvenlik kod incelemesine ilk hangi dosyadan başlanır?
Tek bir doğru dosya yoktur. İlk grup genellikle pom.xml veya Gradle build dosyaları, application-* configuration'ları, deployment manifest'leri ve security bean'leridir. Amaç önce hangi artifact'ın, profile'ın ve security model'in aktif olduğunu belirlemektir.
Önce `SecurityConfig` mi controller mı incelenmeli?
Önce build ve runtime context, ardından bütün security configuration incelenmelidir. Controller daha sonra entry point inventory içinde service ve repository path'iyle birlikte ele alınır.
Spring Security kullanmak uygulamayı güvenli yapar mı?
Spring Security güçlü güvenlik bileşenleri sağlar. Uygulamanın güvenliği matcher, chain order, authentication configuration, method security, object-level authorization ve business implementation'ın doğru kullanımına bağlıdır.
`anyRequest().authenticated()` yeterli midir?
Hayır. Bu rule eşleşen request'ler için authentication gerektirir. Kullanıcının specific action, object veya tenant üzerindeki yetkisini kanıtlamaz.
`@PreAuthorize` kullanmak yeterli midir?
Method security etkin olmalı, çağrı proxy üzerinden geçmeli, expression doğru security requirement'ı uygulamalı ve object-level policy bulunmalıdır. Annotation'ın varlığı çalışma kanıtı değildir.
`permitAll()` güvenlik açığı mıdır?
Endpoint'in public olması beklenmiyorsa veya hassas işlem içeriyorsa açık olabilir. Public login, health veya catalog endpoint'i için bilinçli tercih olabilir. Karar security requirement'a göre verilir.
`csrf.disable()` her zaman yanlış mıdır?
Hayır. Bearer-only ve browser'ın credential'ı otomatik eklemediği stateless API için gerekçeli olabilir. Session cookie, cookie JWT, Basic authentication veya hybrid client varsa risk yeniden değerlendirilmelidir.
JPA SQL injection'ı tamamen engeller mi?
Parameter binding value injection'a karşı güçlü koruma sağlar. Dynamic query fragment, native query concatenation, unsafe sort ve custom JDBC kullanımı hâlâ risk üretebilir.
`@Valid` mass assignment'ı engeller mi?
Hayır. @Valid object üzerindeki validation constraint'lerini çalıştırır. Client'ın hangi field'ı yazabileceğini belirlemez. Dar DTO ve explicit mapping gerekir.
Spring Boot Actuator neden ayrı incelenmeli?
Actuator environment, mapping, log, thread, heap ve operational state hakkında hassas bilgi sunabilir. Exposure ile authorization ayrı kavramlardır. Custom security configuration management endpoint davranışını değiştirebilir.
WebFlux projesi MVC checklist'i ile incelenebilir mi?
Ortak risk sınıfları vardır. Fakat SecurityWebFilterChain, Reactor Context, reactive method security ve context propagation ayrıca incelenmelidir.
Lombok güvenlik açığı mıdır?
Lombok tek başına vulnerability değildir. Generated setter, constructor, equals, hashCode ve toString davranışı writable surface veya sensitive logging'i etkileyebilir. Reviewer generated davranışı anlamalıdır.
SAST Spring code review için yeterli midir?
Hayır. SAST rule ve framework model'i ölçüsünde geniş pattern coverage sağlar. Object ownership, tenant policy, business logic, filter order ve failure behavior için manual review gerekir.
Dependency scan temizse proje güvenli midir?
Hayır. SCA bilinen dependency risklerini araştırır. Custom code, yanlış configuration ve business logic problemlerini doğrulamaz.
Code review sırasında uygulama çalıştırılmalı mı?
Mümkünse evet. Build, bean activation, filter chain, profile ve negative test'leri doğrulamak için runtime validation değerlidir. Fakat güvenilmeyen repository build'i çalıştırmadan önce build script ve plugin'ler incelenmelidir.
Java Spring güvenlik kod incelemesi ne kadar sürer?
Module sayısı, source size, framework çeşitliliği, role ve tenant modeli, business logic, build hazırlığı ve runtime validation kapsamına göre değişir. Satır sayısı tek başına yeterli efor ölçüsü değildir.
Review sonrası retest nasıl yapılmalı?
Original finding path yeniden doğrulanmalı, aynı root cause'un benzer örnekleri aranmalı ve negative regression test çalıştırılmalıdır. Yalnızca vulnerable line'ın değişmiş olması sistemik düzeltmeyi kanıtlamaz.
Okumaya devam et
Kaynak Kod Analizi
Secure Code Review ile Otomatik SAST Taraması Farkı
SAST kurallarla ifade edilebilen code pattern ve data flow'ları ölçekli biçimde tarar. Secure Code Review ise security requirement, business logic ve implementation context'ini insan muhakemesiyle değerlendirir.
Yazıyı okuKaynak Kod Analizi
SAST Nedir, Hangi Açıkları Bulur, Hangilerini Kaçırır?
SAST, uygulama çalıştırılmadan source code, bytecode veya binary üzerinde güvenlik analizi yapar. Injection ve riskli API kullanımında güçlüdür; ancak business logic, IDOR ve runtime configuration gibi kritik riskleri tek başına kapsayamaz.
Yazıyı oku