We make a lot of use of modern software development frameworks. We get rid of repetitive work, adopt certain standards and at the end of the day, we are involved in a much more proactive software development process.

But in the shadow of so many advantages lies a danger. This dangerous illusion is as follows;

"I'm using a framework, so my app is secure."

This illusion is based on what is known in the security ecosystem as security-by-convention. While the security measures that come by default in frameworks, conventions, automated security products all offer us a perception of secure, there are actually some gaps.

Psychological Background. Why Do We Trust Frameworks?

Human beings are psychologically inclined to believe in authority and what is common and to shake off the responsibility of trust. This leads to the act of "outsourcing" security during development. The place of this in our concept lies in the belief that frameworks basically care about security and protect it adequately. While this may have a theoretical basis and is generally the case with highly popular frameworks, unfortunately this is not always the case.

The Impact of Automation Provided by Modern Frameworks on Security

Django Rest Api Example

As you can see in the Django example, the Django Rest Framework comes with the AllowAny feature by default. This means that your api endpoint can be accessed without authentication. One of the problems that is likely to be overlooked.

from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .models import User
from .serializers import UserSerializer

# VULNERABLE EXAMPLE
# AllowAny is the default permission if not specified
class VulnerableUserViewSet(viewsets.ModelViewSet):
    """
    This ViewSet is vulnerable because it doesn't specify permission_classes.
    By default, Django REST Framework uses AllowAny, which means anyone can access this API.
    """
    queryset = User.objects.all()
    serializer_class = UserSerializer
    # Missing permission_classes = [IsAuthenticated]


# SECURE EXAMPLE
class SecureUserViewSet(viewsets.ModelViewSet):
    """
    This ViewSet is secure because it explicitly defines permission_classes
    to require authentication for all operations.
    """
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [IsAuthenticated]  # Explicitly defined
    
    # Extra security: Users can only see/modify their own data
    def get_queryset(self):
        user = self.request.user
        if user.is_staff:
            return User.objects.all()
        return User.objects.filter(id=user.id)

Spring Boot Example

It is a common practice to disable CSRF protection in Spring Boot's REST API. It is based on the assumption that the REST API does not need CSRF protection because it is stateless. However, this assumption does not apply to APIs that use cookie-based authentication. Such APIs may be vulnerable to CSRF attacks.

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

// VULNERABLE EXAMPLE
@Configuration
@EnableWebSecurity
public class VulnerableSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .httpBasic();
        
        // DANGEROUS: CSRF protection disabled!
        // Common misconception: "It's a REST API, so we don't need CSRF protection"
        http.csrf().disable();
    }
}

// SECURE EXAMPLE
@Configuration
@EnableWebSecurity
public class SecureSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .httpBasic();
        
        // Properly configured CSRF protection
        http.csrf()
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            // Only disable CSRF for specific public endpoints if necessary
            .ignoringAntMatchers("/api/public/**");
    }
}

Security-by-Convention vs. Security-by-Design

Conventional approaches assume that the default behavior of frameworks is secure.

Passive: Does not explicitly specify security controls
It is based on assumptions: Based on assumptions about what the Framework does
It is invisible: It is difficult to identify what is missing in the code
Fragile: Behavior may change with framework updates

The security design approach is slightly different.

Active: Each safety control is clearly defined
It is deliberate: Every security decision is deliberate
Visible: Security controls are clearly stated in the code
Resilient: Framework changes do not affect your understanding of security

If we are going to give an example through code, the middleware queue is an important point in express.js. The conceptual approach proceeds from the assumption that "middleware is used, therefore it is safe". In the security design structure, it has the "middleware order is important. every route must be protected" approach.

const express = require('express');
const jwt = require('express-jwt');

// VULNERABLE EXAMPLE
const vulnerableApp = express();

// Dangerous: Routes defined BEFORE authentication middleware
vulnerableApp.get('/api/users', (req, res) => {
  // This endpoint is completely unprotected!
  const users = [
    { id: 1, name: 'Admin User', email: 'admin@example.com' }, 
    { id: 2, name: 'Regular User', email: 'user@example.com' }
  ];
  
  res.json({ users: users });
});

vulnerableApp.get('/api/settings', (req, res) => {
  // This endpoint is also unprotected!
  res.json({ 
    apiKeys: {
      stripe: 'sk_test_123456789',
      aws: 'AKIA1234567890EXAMPLE'
    } 
  });
});

// Authentication middleware added TOO LATE
// Only routes defined AFTER this line will be protected
vulnerableApp.use(jwt({ 
  secret: 'your-secret-key',
  algorithms: ['HS256']
}));


// SECURE EXAMPLE - APPROACH 1: Global middleware first
const secureApp = express();

// Authentication middleware added FIRST
secureApp.use(jwt({ 
  secret: 'your-secret-key',
  algorithms: ['HS256']
}));

// All routes defined AFTER middleware are protected
secureApp.get('/api/users', (req, res) => {
  // This endpoint is now protected
  res.json({ users: 'Protected data' });
});


// SECURE EXAMPLE - APPROACH 2: Route-specific middleware
const routeSecureApp = express();

// Define middleware but don't apply globally
const jwtMiddleware = jwt({ 
  secret: 'your-secret-key',
  algorithms: ['HS256']
});

// Public route without middleware
routeSecureApp.get('/api/public', (req, res) => {
  res.json({ message: 'Public data' });
});

// Protected route with explicitly applied middleware
routeSecureApp.get('/api/users', jwtMiddleware, (req, res) => {
  // This endpoint is protected
  res.json({ users: 'Protected data' });
});

module.exports = { vulnerableApp, secureApp, routeSecureApp };

Proactive Nature of Security-by-Design vs. Reactive Behavior of Convention

Security-by-Design puts security at the center of the design from the start. Instead of guessing what the framework will do, it explicitly specifies each security check.

Rails' default CSRF protection works for form-based requests, but requiring additional protection for JSON requests should be proactive, not reactive.

# VULNERABLE EXAMPLE
class VulnerableApplicationController < ActionController::Base
  # Default CSRF protection only works for form submissions
  protect_from_forgery with: :exception
  
  # No explicit protection for JSON API requests
  # This creates a security gap for JSON POST requests
end

class VulnerableApiController < VulnerableApplicationController
  def update_user
    # This endpoint is vulnerable to CSRF attacks via JSON
    @user = User.find(params[:id])
    @user.update(user_params)
    render json: @user
  end
  
  private
  
  def user_params
    params.require(:user).permit(:name, :email, :role)
  end
end

# How this can be exploited:
# =========================
# 


# SECURE EXAMPLE
class SecureApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  
  # Add explicit protection for JSON requests
  before_action :verify_api_request_authenticity, if: :json_request?
  
  private
  
  def json_request?
    request.format.json? || request.content_type == 'application/json'
  end
  
  def verify_api_request_authenticity
    unless valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
      render json: { error: 'Invalid CSRF token' }, status: :unprocessable_entity
    end
  end
end

class SecureApiController < SecureApplicationController
  def update_user
    # Authentication check
    unless current_user && current_user.can_edit_user?(params[:id])
      return render json: { error: 'Unauthorized' }, status: :forbidden
    end
    
    # Action logging
    Rails.logger.info("User #{current_user.id} updating user #{params[:id]}")
    
    @user = User.find(params[:id])
    @user.update(user_params)
    render json: @user
  end
  
  private
  
  def user_params
    # More restricted parameters
    params.require(:user).permit(:name, :email)
  end
end

Bonus Examples (Angular / React)

If we need to give an example on React, the "useNavigate" hook is used directly. React Router does not perform URL validation by default. This leads to open redirect vulnerability.

React

import React from 'react';
import { useNavigate, useLocation } from 'react-router-dom';

// VULNERABLE: Unsafe redirect
export function VulnerableRedirect() {
  const navigate = useNavigate();
  const redirectTo = new URLSearchParams(useLocation().search).get("redirectTo");
  return ;
}

// SECURE: Safe redirect with validation
export function SecureRedirect() {
  const navigate = useNavigate();
  const redirectTo = new URLSearchParams(useLocation().search).get("redirectTo");
  
  const handleRedirect = () => {
    try {
      const url = new URL(redirectTo);
      const isAllowed = ['trusted-site.com'].some(d => url.hostname.endsWith(d));
      navigate(isAllowed ? redirectTo : '/dashboard');
    } catch {
      navigate('/dashboard');
    }
  };

  return ;
}

// Example usage
function App() {
  return (
    

Vulnerable Redirect (Don't use this!)

Secure Redirect (Use this!)

); }

Angular

To give a different example over Angular, the template interpolation mechanism can be given as an example. There is no input sanitization that comes by default on user templates.

import { Component } from '@angular/core';

// VULNERABLE: Template injection
@Component({
  selector: 'app-vulnerable',
  template: `
{{ userInput }}
` }) export class VulnerableComponent { userInput = '{{constructor.constructor(\'alert("TomSample")\')()}}'; } // SECURE: Safe template @Component({ selector: 'app-secure', template: `
` }) export class SecureComponent { sanitizedInput: string; constructor() { this.sanitizedInput = this.sanitize('User Input'); } private sanitize(input: string): string { return input .replace(/&/g, '&') .replace(//g, '>'); } } // Example usage in app module @Component({ selector: 'app-root', template: `

Vulnerable Profile (Don't use this!)

Secure Profile (Use this!)

` }) export class AppComponent {}

Framework Vulnerability Matrix: Platform Specific Risk Types

Each framework has its own security assumptions and accordingly unique vulnerability types. The table below shows the typical security gaps in different frameworks that we examined through the examples in our project.

Framework Vulnerability Matrix
Framework Vulnerability Matrix: Platform Specific Risk Types

What is striking about this matrix is that each framework has "positive assumptions" and developers tend to accept these assumptions without question. However, security requires pessimistic, not optimistic thinking — instead of "it is probably safe", it should be "it is unsafe until proven to be clearly safe".

Contrary to popular belief, security vulnerabilities are not always caused by "misconfiguration". They are caused by " missing configuration".

Misconfiguration ❌

Missing Configuration ✅

Stay Safe.