DEV Community

Cover image for Sicurezza API REST: best practices
Cub4nH1
Cub4nH1

Posted on

Sicurezza API REST: best practices

Meta description: Scopri le best practice per la sicurezza delle API REST. Guida completa con esempi di codice, autenticazione, autorizzazione e protezione contro le vulnerabilità più comuni.


Le API REST sono il backbone delle applicazioni moderne. Dalle app mobile ai microservizi, dalle integrazioni SaaS alle piattaforme cloud, le API REST permettono la comunicazione tra sistemi diversi in modo standardizzato e scalabile. Tuttavia, questa onnipresenza le rende anche un bersaglio privilegiato per gli attaccanti.

Proteggere le API REST non è più un optional: è una necessità imprescindibile. In questa guida, esploreremo le best practice essenziali per garantire la sicurezza delle tue API, con esempi pratici di codice e strategie collaudate.

Perché la sicurezza delle API REST è critica

Le API REST espongono dati e funzionalità direttamente al mondo esterno. A differenza delle interfacce web tradizionali, le API sono progettate per essere consumate da macchine, il che significa che un attaccante può automatizzare gli attacchi e testare migliaia di combinazioni in pochi secondi.

Le conseguenze di un'API compromessa possono essere devastanti: furto di dati sensibili, accesso non autorizzato a sistemi interni, violazione della privacy degli utenti e danni reputazionali significativi. Secondo recenti studi, gli attacchi alle API sono tra le prime cause di data breach nel 2026.

Autenticazione e Autenticazione: i fondamenti

OAuth 2.0 e OpenID Connect

OAuth 2.0 è lo standard de facto per l'autorizzazione delle API. OpenID Connect aggiunge un livello di autenticazione sopra OAuth 2.0.

# Implementazione OAuth 2.0 con Flask
from authlib.integrations.flask_client import OAuth
from flask import Flask, jsonify, request

app = Flask(__name__)
oauth = OAuth(app)

# Configurazione provider OAuth
oauth.register(
    name='google',
    client_id='your-client-id',
    client_secret='your-client-secret',
    server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
    client_kwargs={'scope': 'openid email profile'}
)

@app.route('/api/auth/login')
def login():
    redirect_uri = url_for('authorize', _external=True)
    return oauth.google.authorize_redirect(redirect_uri)

@app.route('/api/auth/callback')
def authorize():
    token = oauth.google.authorize_access_token()
    user_info = token.get('userinfo')

    if user_info:
        # Crea o aggiorna l'utente nel database
        user = User.get_or_create(
            email=user_info['email'],
            name=user_info['name']
        )
        # Genera token JWT per l'utente
        access_token = generate_jwt_token(user.id)
        return jsonify({'token': access_token})

    return jsonify({'error': 'Autenticazione fallita'}), 401
Enter fullscreen mode Exit fullscreen mode

JWT (JSON Web Tokens)

I JWT sono ampiamente utilizzati per l'autenticazione stateless delle API. Tuttavia, richiedono un'implementazione corretta per evitare vulnerabilità.

import jwt
import uuid
from datetime import datetime, timedelta
from functools import wraps
from flask import request, jsonify

JWT_SECRET = 'your-256-bit-secret-key'  # Usa variabile d'ambiente
JWT_ALGORITHM = 'HS256'

def generate_token_pair(user_id):
    """Genera access token e refresh token"""
    access_payload = {
        'sub': user_id,
        'iat': datetime.utcnow(),
        'exp': datetime.utcnow() + timedelta(minutes=15),
        'type': 'access',
        'jti': str(uuid.uuid4())
    }

    refresh_payload = {
        'sub': user_id,
        'iat': datetime.utcnow(),
        'exp': datetime.utcnow() + timedelta(days=7),
        'type': 'refresh',
        'jti': str(uuid.uuid4())
    }

    access_token = jwt.encode(access_payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
    refresh_token = jwt.encode(refresh_payload, JWT_SECRET, algorithm=JWT_ALGORITHM)

    return access_token, refresh_token

def verify_token(f):
    """Decoratore per verificare JWT nelle richieste"""
    @wraps(f)
    def decorated(*args, **kwargs):
        auth_header = request.headers.get('Authorization', '')

        if not auth_header.startswith('Bearer '):
            return jsonify({'error': 'Token mancante'}), 401

        token = auth_header.replace('Bearer ', '')

        try:
            payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])

            if payload['type'] != 'access':
                return jsonify({'error': 'Tipo token non valido'}), 401

            # Verifica revoca token
            if is_token_revoked(payload['jti']):
                return jsonify({'error': 'Token revocato'}), 401

            request.user_id = payload['sub']

        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token scaduto'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Token non valido'}), 401

        return f(*args, **kwargs)
    return decorated

@app.route('/api/protected')
@verify_token
def protected_resource():
    return jsonify({'message': 'Accesso consentito', 'user_id': request.user_id})
Enter fullscreen mode Exit fullscreen mode

Autorizzazione: controllare l'accesso alle risorse

Role-Based Access Control (RBAC)

from enum import Enum
from functools import wraps

class Role(Enum):
    ADMIN = 'admin'
    EDITOR = 'editor'
    VIEWER = 'viewer'

def require_role(*roles):
    """Decoratore per verificare i ruoli dell'utente"""
    def decorator(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            user = User.query.get(request.user_id)

            if not user or user.role not in [r.value for r in roles]:
                return jsonify({'error': 'Permesso negato'}), 403

            return f(*args, **kwargs)
        return decorated
    return decorator

@app.route('/api/admin/users', methods=['GET'])
@verify_token
@require_role(Role.ADMIN)
def list_users():
    users = User.query.all()
    return jsonify([u.to_dict() for u in users])

@app.route('/api/content', methods=['POST'])
@verify_token
@require_role(Role.ADMIN, Role.EDITOR)
def create_content():
    # Solo admin e editor possono creare contenuti
    pass
Enter fullscreen mode Exit fullscreen mode

Attribute-Based Access Control (ABAC)

def check_resource_access(user, resource, action):
    """
    Verifica l'accesso basato su attributi
    Esempio: l'utente può modificare solo le proprie risorse
    """
    if action == 'read':
        return True  # Tutti possono leggere

    if action == 'update' or action == 'delete':
        # Solo il proprietario o un admin può modificare/eliminare
        return resource.owner_id == user.id or user.role == 'admin'

    return False

@app.route('/api/resources/<int:resource_id>', methods=['PUT'])
@verify_token
def update_resource(resource_id):
    user = User.query.get(request.user_id)
    resource = Resource.query.get_or_404(resource_id)

    if not check_resource_access(user, resource, 'update'):
        return jsonify({'error': 'Non autorizzato'}), 403

    # Procedi con l'aggiornamento
    data = request.get_json()
    resource.update(data)
    db.session.commit()

    return jsonify(resource.to_dict())
Enter fullscreen mode Exit fullscreen mode

Validazione e Sanificazione degli Input

Validazione con Pydantic

from pydantic import BaseModel, EmailStr, validator, Field
from typing import Optional
import re

class UserCreateSchema(BaseModel):
    username: str = Field(..., min_length=3, max_length=30)
    email: EmailStr
    password: str = Field(..., min_length=12)
    age: Optional[int] = Field(None, ge=13, le=120)

    @validator('username')
    def validate_username(cls, v):
        if not re.match(r'^[a-zA-Z0-9_]+$', v):
            raise ValueError('Username può contenere solo lettere, numeri e underscore')
        return v

    @validator('password')
    def validate_password(cls, v):
        if not re.search(r'[A-Z]', v):
            raise ValueError('La password deve contenere almeno una maiuscola')
        if not re.search(r'[a-z]', v):
            raise ValueError('La password deve contenere almeno una minuscola')
        if not re.search(r'\d', v):
            raise ValueError('La password deve contenere almeno un numero')
        if not re.search(r'[!@#$%^&*]', v):
            raise ValueError('La password deve contenere almeno un carattere speciale')
        return v

@app.route('/api/users', methods=['POST'])
def create_user():
    try:
        data = UserCreateSchema(**request.get_json())
    except ValidationError as e:
        return jsonify({'error': e.errors()}), 400

    # Crea l'utente con dati validati
    user = User.create(
        username=data.username,
        email=data.email,
        password=hash_password(data.password)
    )

    return jsonify(user.to_dict()), 201
Enter fullscreen mode Exit fullscreen mode

Rate Limiting e Throttling

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import redis

limiter = Limiter(
    app=app,
    key_func=get_remote_address,
    storage_uri="redis://localhost:6379",
    default_limits=["100 per minute", "1000 per hour"]
)

# Rate limiting per endpoint specifico
@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
    # Logica di login
    pass

# Rate limiting basato sull'utente autenticato
def get_user_key():
    return str(getattr(request, 'user_id', get_remote_address()))

@app.route('/api/data')
@limiter.limit("60 per minute", key_func=get_user_key)
def get_data():
    # Dati per utente autenticato
    pass
Enter fullscreen mode Exit fullscreen mode

Gestione sicura degli errori

from flask import jsonify
from werkzeug.exceptions import HTTPException

@app.errorhandler(Exception)
def handle_exception(e):
    """Gestore globale degli errori che non espone dettagli interni"""

    # Log dell'errore completo per il team di sviluppo
    app.logger.error(f"Errore non gestito: {str(e)}", exc_info=True)

    if isinstance(e, HTTPException):
        return jsonify({
            'error': e.name,
            'message': e.description
        }), e.code

    # Per errori generici, non esporre dettagli interni
    return jsonify({
        'error': 'Internal Server Error',
        'message': 'Si è verificato un errore imprevisto'
    }), 500

# Risposte di errore personalizzate
@app.errorhandler(404)
def not_found(e):
    return jsonify({
        'error': 'Not Found',
        'message': 'La risorsa richiesta non esiste'
    }), 404

@app.errorhandler(429)
def rate_limited(e):
    return jsonify({
        'error': 'Too Many Requests',
        'message': 'Hai superato il limite di richieste. Riprova più tardi.'
    }), 429
Enter fullscreen mode Exit fullscreen mode

Logging e Monitoraggio

import logging
import json
from datetime import datetime, timezone

class APILogger:
    def __init__(self):
        self.logger = logging.getLogger('api')
        handler = logging.FileHandler('api.log')
        handler.setFormatter(logging.Formatter('%(message)s'))
        self.logger.addHandler(handler)
        self.logger.setLevel(logging.INFO)

    def log_request(self, request, response, user_id=None):
        log_entry = {
            'timestamp': datetime.now(timezone.utc).isoformat(),
            'method': request.method,
            'path': request.path,
            'status_code': response.status_code,
            'ip_address': request.remote_addr,
            'user_agent': request.headers.get('User-Agent'),
            'user_id': user_id,
            'duration_ms': getattr(request, 'duration', None)
        }
        self.logger.info(json.dumps(log_entry))

    def log_security_event(self, event_type, details, severity='WARNING'):
        log_entry = {
            'timestamp': datetime.now(timezone.utc).isoformat(),
            'event_type': event_type,
            'details': details,
            'severity': severity,
            'ip_address': request.remote_addr
        }
        self.logger.warning(json.dumps(log_entry))

api_logger = APILogger()

@app.after_request
def log_response(response):
    api_logger.log_request(request, response, getattr(request, 'user_id', None))
    return response
Enter fullscreen mode Exit fullscreen mode

Sicurezza dei dati sensibili

Crittografia end-to-end

from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
import os

class DataEncryptor:
    def __init__(self, master_key: str):
        """Inizializza l'encryptor con una chiave master"""
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=os.urandom(16),
            iterations=480000,
        )
        key = base64.urlsafe_b64encode(kdf.derive(master_key.encode()))
        self.fernet = Fernet(key)

    def encrypt(self, data: str) -> str:
        """Cifra dati sensibili"""
        return self.fernet.encrypt(data.encode()).decode()

    def decrypt(self, encrypted_data: str) -> str:
        """Decifra dati sensibili"""
        return self.fernet.decrypt(encrypted_data.encode()).decode()

# Utilizzo
encryptor = DataEncryptor(os.environ['MASTER_KEY'])

# Cifra dati prima del salvataggio
encrypted_ssn = encryptor.encrypt(user.ssn)
# Decifra quando necessario
ssn = encryptor.decrypt(encrypted_ssn)
Enter fullscreen mode Exit fullscreen mode

Versionamento e documentazione sicura

OpenAPI/Swagger con sicurezza

openapi: 3.0.0
info:
  title: Secure API
  version: 1.0.0
  description: API con best practice di sicurezza

security:
  - BearerAuth: []

paths:
  /api/users:
    get:
      summary: Lista utenti
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Lista utenti
        '401':
          description: Non autorizzato
        '403':
          description: Permesso negato

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
Enter fullscreen mode Exit fullscreen mode

Checklist di sicurezza per API REST

  • [ ] Autenticazione JWT con refresh token
  • [ ] Autorizzazione RBAC/ABAC implementata
  • [ ] Validazione input lato server
  • [ ] Rate limiting configurato
  • [ ] HTTPS obbligatorio
  • [ ] CORS configurato correttamente
  • [ ] Logging e monitoraggio attivi
  • [ ] Gestione errori sicura
  • [ ] Crittografia dati sensibili
  • [ ] Versionamento API
  • [ ] Documentazione OpenAPI aggiornata
  • [ ] Test di sicurezza automatizzati

Conclusione

La sicurezza delle API REST richiede un approccio multilivello che copre autenticazione, autorizzazione, validazione, logging e monitoraggio. Ogni strato aggiunge una barriera protettiva che rende gli attacchi più difficili e costosi per gli aggressori.

Ricorda che la sicurezza è un processo continuo. Aggiorna regolarmente le dipendenze, esegui test di sicurezza periodici e mantieniti informato sulle ultime vulnerabilità e tecniche di attacco.

Vuoi approfondire la sicurezza delle API? Iscriviti alla nostra newsletter per ricevere guide settimanli, case study e aggiornamenti sulle best practice di sicurezza. Condividi questo articolo con il tuo team e aiutaci a costruire API più sicure per tutti!

Top comments (0)