CryptoFolio è un'applicazione web incentrata sulla sicurezza, progettata per aiutare gli utenti a tenere traccia e a gestire i loro investimenti in criptovalute, con particolare attenzione alla protezione dei dati e all'autenticazione sicura. Basato su Flask , con le moderne politiche di sicurezza offre un robusto set di funzionalità mantenendo rigorosi controlli di sicurezza.
Laurea Magistrale in Sicurezza informatica - Università degli studi di Bari "Aldo Moro" - Dipartimento d'Informatica
L'applicazione consente agli utenti di:
- 📊 Tracciare le proprie criptovalute e i loro valori di mercato attuali
- 📈 Monitorare le prestazioni del portfolio e le metriche di profitto/perdita
- ✏️ Aggiungere, aggiornare e rimuovere posizioni in criptovalute
- 📉 Visualizzare le analisi del portfolio
- 💹 Accedere ai prezzi delle criptovalute in tempo reale grazie all'integrazione delle API di CoinGecko
- 🔐 Gestire il portfolio in modo sicuro attraverso l'autenticazione OAuth 2.0
- 🔄 Interagire con il proprio portfolio attraverso un'API Restful
- 💱 Ottenere il controvalore del proprio portfolio in diverse valute sfrutando i tassi di conversione aggiornati in tempo reale
CryptoFolio implementa un modello di "architettura orientata ai servizi (SOA)", in particolare un approccio simile ai microservizi, in cui i diversi componenti hanno un basso accoppiamento ed un'alta coesione, comunicando attraverso interfacce ben definite (API REST).
Questa scelta architettonica offre diversi vantaggi:
- Indipendenza dai servizi: Ogni componente (autenticazione, gestione del portafoglio...) opera in modo indipendente
- Scalabilità: I servizi possono essere scalati in modo indipendente in base al carico
- Isolamento della sicurezza: La compromissione di un servizio non compromette automaticamente gli altri
- Flessibilità tecnologica: Servizi diversi possono utilizzare tecnologie diverse a seconda delle necessità
Flask è un framework di sviluppo web open-source scritto in Python. È stato progettato per essere un framework minimalista, flessibile e facile da utilizzare per la creazione di applicazioni web. L'utilizzo di Flask non è casuale, poichè consente lo sviluppo di API (Interfacce di Programmazione delle Applicazioni) attraverso la creazione di endpoint e percorsi per elaborare richieste e risposte HTTP.
Le caratteristiche principali includono:
-
Nucleo minimalista e flessibile
-
Ampio supporto per i plugin
-
Server di sviluppo integrato
-
Integrazione del toolkit Werkzeug per le funzionalità WSGI
Il Web Server Gateway Interface (WSGI) è un protocollo di trasmissione che stabilisce e descrive comunicazioni ed interazioni tra server ed applicazioni web scritte nel linguaggio Python. È quindi l'interfaccia standard del web service per la programmazione in Python.
In CryptoFolio, Flask gestisce:
- Instradamento ed elaborazione delle richieste
- Gestione delle sessioni
- Rendering dei modelli
- Integrazione del middleware di sicurezza
- Implementazione di endpoint API
- Configurazione iniziale del Server
- Meccanismi di sicurezza
Il frontend utilizza uno stack moderno incentrato su sicurezza e prestazioni:
- HTML5/CSS3: markup semantico e stile reattivo
- JavaScript (ES6+): Funzionalità lato client con modelli sicuri
- Bootstrap 5: framework di design reattivo
- Select2: Caselle di selezione migliorate
- Chart.js: Visualizzazione grafica del portafoglio
- jQuery: Manipolazione del DOM e richieste AJAX
La tecnologia utilizzata ha permesso l'Implementazione di CSP (Content Security Policy) e quindi di conseguenza misure di prevenzione XSS, gestione dei token CSRF attraverso cookie sicuri, sanificazione degli input e una corretta gestione degli errori per evitare IOE (Information over exposure).
Firebase Cloud Firestore è un cloud NoSQL flessibile e scalabile, costruito sull'infrastruttura di Google Cloud, per archiviare e sincronizzare i dati per lo sviluppo lato client e lato server. Firebase fornisce una soluzione di database sicura e scalabile con:
- Sincronizzazione dei dati in tempo reale
- Integrazione dell'autenticazione integrata
- Scalabilità automatica
- Regole di accesso sicuro ai dati
- Backup e disaster recovery
- Archiviazione crittografata dei dati
Sono state implementate - attraverso API debitamente protette - diversi servizi di sicurezza che lavorano sinergicamente per proteggere l'applicazione:
Servizio di autenticazione
Integrazione OAuth 2.0 (Google/GitHub)
Gestione dei token JWT
Gestione delle sessioni
Gestione dell'identità dell'utente
Gestione delle origini
Controllo Cookie e Headers
Servizio di crittografia
Crittografia AES-256
Hashing con HMAC
Gestione delle chiavi
Archiviazione sicura dei dati
Crittografia dei token
Servizio di limitazione
Limitazione delle richieste
Protezione DDoS
Monitoraggio dell'utilizzo
Servizio di protezione CSRF
Generazione di token
Convalida delle richieste
Gestione nonce
Il sistema per offrire le funzionalità core dello stesso, integra diversi servizi esterni attraverso API RestFul messe a disposizione. Nel caso specifico dei prezzi crypto - per garantire disponibilità del dato agli utenti - è stato implementato un meccanismo di caching interno che possa aggirare la problematica della "sincronia" delle API RestFul, ovvero una delle caratteristiche principali dei Servizi Rest. Se Coincegko non dovesse essere disponibile, l'utente potrà in egual modo accedere ai prezzi delle crypto grazie a tale sistema.
API CoinGecko
- Prezzi delle criptovalute in tempo reale
- Dati di mercato
- Informazioni sulle attività
Fornitori OAuth
- Servizio OAuth di Google
- Servizio GitHub OAuth
Ngrok
- Tunnel sicuro per lo sviluppo locale
- Crittografia TLS e utilizzo del protocollo HTTPS
- Gestione degli URL
Il flusso di comunicazione del sistema segue diversi schemi chiave:
Comunicazione utente-frontend
- Connessioni protette da HTTPS
- Invio di moduli
- Richieste AJAX
Comunicazione Frontend-Backend
- Chiamate API REST
- Autenticazione JWT
- Convalida dei token CSRF
- Richieste limitate
Comunicazione Backend-Servizi esterni
- Chiamate API a CoinGecko
- Operazioni sul database Firebase
- Interazioni con il provider OAuth
- Gestione del tunnel Ngrok
- Comunicazione con i servizi interni
- Autenticazione da servizio a servizio
- Trasferimento di dati crittografati
In tutto il sistema, i dati sono protetti da:
Sicurezza del trasporto
- Crittografia TLS 1.3
- Convalida del certificato
- Applicazione del protocollo sicuro
Sicurezza dell'archiviazione dei dati
- Crittografia AES-256
- Gestione sicura delle chiavi
- Generazione del sale
Sicurezza delle richieste
- Convalida dell'origine
- Sanificazione dell'input e output
- Limitazione delle richieste
- Protezione CSRF
Le comunicazioni avvengono tutte attraverso le cosìddette Routes, messe a disposizione di Flask. Ogni route rappresenta quindi un endpoint accessibile attraverso un' API Restful con un livello di protezione adeguato alla route specifica.
Link all'architettura completa: CryptoFolio(Project).pdf
Google implementa OpenID Connect, un'estensione di OAuth 2.0 che fornisce un layer di identità standardizzato. Questo è evidenziato nel codice attraverso la configurazione specifica:
# Configurazione Google OAuth con OpenID Connect
oauth.register(
name='google',
server_metadata_url=os.getenv('SERVER_METADATA_URL_GOOGLE'),
client_kwargs={
'scope': 'openid email profile', # Scopes OpenID Connect specifici
}
)
L'utilizzo di OpenID Connect è identificabile attraverso:
- L'endpoint di metadata
.well-known/openid-configuration
- Gli scope standardizzati
openid
,email
eprofile
- La ricezione automatica delle informazioni dell'utente attraverso l'ID token
Il processo avviene principalmente attraverso il meccanismo di "OpenID Connect Discovery" implementato nella libreria Authlib.
Nel file init_app.py
:
oauth.register(
name='google',
client_id=os.getenv('GOOGLE_CLIENT_ID'),
client_secret=os.getenv('GOOGLE_CLIENT_SECRET'),
server_metadata_url=os.getenv('SERVER_METADATA_URL_GOOGLE'),
client_kwargs={
'scope': 'openid email profile',
'ssl_verify': True,
'token_endpoint_auth_method': 'client_secret_post'
},
redirect_uri=f"{base_url}/auth/callback/google"
)
La chiave è il parametro server_metadata_url
, che nel file .env
è impostato a:
SERVER_METADATA_URL_GOOGLE=https://accounts.google.com/.well-known/openid-configuration
Questo URL è il punto di ingresso del processo di "discovery". Quando l'applicazione si inizializza:
-
Authlib fa una richiesta GET a https://accounts.google.com/.well-known/openid-configuration
-
Google risponde con un documento JSON che contiene tutti gli endpoint necessari. La risposta sarà del tipo:
{
"issuer": "https://accounts.google.com",
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"token_endpoint": "https://oauth2.googleapis.com/token",
"userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
...
}
- Authlib memorizza questi endpoint e li usa automaticamente quando necessario. Per esempio:
- Per l'autenticazione iniziale usa
authorization_endpoint
- Per ottenere il token di accesso usa
token_endpoint
- Per recuperare le informazioni dell'utente usa
userinfo_endpoint
- Per l'autenticazione iniziale usa
Difatti nella callback:
# Prima ottiene il token usando internamente token_endpoint
token = client.authorize_access_token()
# Poi usa userinfo_endpoint per ottenere i dati dell'utente
user_info = client.get(
'https://www.googleapis.com/oauth2/v3/userinfo',
token=token
).json()
Questo approccio basato sul discovery ha diversi vantaggi:
- Sicurezza: gli endpoint sono forniti direttamente da Google invece di essere hardcodati
- Manutenibilità: se Google cambia i suoi endpoint, l'applicazione continuerà a funzionare
- Standardizzazione: segue le specifiche OpenID Connect per il discovery
Per GitHub, che non supporta OpenID Connect Discovery, gli endpoint devono essere configurati manualmente:
oauth.register(
name='github',
client_id=os.getenv('GITHUB_CLIENT_ID'),
client_secret=os.getenv('GITHUB_CLIENT_SECRET'),
access_token_url='https://github.com/login/oauth/access_token',
authorize_url='https://github.com/login/oauth/authorize',
client_kwargs={...}
)
La differenza tra i due approcci mostra il vantaggio di usare provider che supportano OpenID Connect Discovery.
GitHub utilizza invece OAuth 2.0 standard, come dimostrato dalla configurazione esplicita degli endpoint e degli scope specifici della piattaforma:
# Configurazione GitHub OAuth standard
oauth.register(
name='github',
access_token_url='https://github.com/login/oauth/access_token',
authorize_url='https://github.com/login/oauth/authorize',
client_kwargs={
'scope': 'read:user user:email', # Scope specifici GitHub
}
)
Entrambi i provider implementano il flusso Authorization Code Grant, che si articola nelle seguenti fasi:
- Inizializzazione Autorizzazione
@app.route('/auth/login/<provider>')
def login(provider):
csrf_token, response = csrf.generate_token(require_user_id=False)
session['csrf_token'] = csrf_token
oauth_client = oauth.create_client(provider)
return oauth_client.authorize_redirect(callback_url)
- Gestione Callback
@app.route('/auth/callback/<provider>')
def auth_callback(provider):
if request.args.get('state') != session.get('csrf_token'):
raise ValueError("Invalid CSRF state")
client = oauth.create_client(provider)
token = client.authorize_access_token()
- Recupero Informazioni Utente
La differenza principale tra i due provider si manifesta qui:
Per Google (OpenID Connect):
user_info = client.get('userinfo').json() # Endpoint standardizzato
Per GitHub (OAuth 2.0):
user_info = client.get('https://api.github.com/user').json()
email_response = client.get('https://api.github.com/user/emails') # Richiesta aggiuntiva necessaria
Il sistema implementa diverse misure di sicurezza:
- Protezione CSRF
csrf_token = session['csrf_token'] # Token per prevenire attacchi CSRF
- Validazione dello Stato
if request.args.get('state') != session.get('csrf_token'):
raise ValueError("Invalid CSRF state")
- Gestione Sicura delle Sessioni
session.clear() # Pulizia della sessione precedente
session['user_id'] = user_id # Memorizzazione sicura dell'ID utente
Il parametro state
(che trasporta il token CSRF) è infatti un requisito di sicurezza fondamentale nel protocollo OAuth 2.0, verificato da entrambi i provider.
Entrambi i provider mantengono il valore del parametro state
durante tutto il flusso di autorizzazione. Quando reindirizzano l'utente alla callback URL, includono lo stesso valore di state
che hanno ricevuto. Questo comportamento è standardizzato nella specifica OAuth 2.0 (RFC 6749, Sezione 4.1.1).
Per esempio, quando GitHub riceve una richiesta di autorizzazione:
https://github.com/login/oauth/authorize?
client_id=YOUR_CLIENT_ID&
redirect_uri=YOUR_CALLBACK&
state=CSRF_TOKEN
Restituirà una risposta alla callback URL nella forma:
YOUR_CALLBACK?code=AUTHORIZATION_CODE&state=CSRF_TOKEN
Nel codice vediamo la verifica:
@app.route('/auth/callback/<provider>')
def auth_callback(provider):
if request.args.get('state') != session.get('csrf_token'):
raise ValueError("Invalid CSRF state")
Questa verifica è obbligatoria per due motivi:
-
Specifiche OAuth 2.0: La RFC 6749 stabilisce che:
"Se il parametro 'state' era presente nella richiesta di autorizzazione, lo STESSO valore DEVE essere restituito nella risposta."
-
Requisiti dei Provider: Sia Google che GitHub rifiuteranno di completare il flusso di autenticazione se:
- Il parametro
state
non è presente nella richiesta iniziale - Il client non verifica la corrispondenza del
state
nella callback
- Il parametro
Questo doppio livello di controllo (provider e client) rende il sistema particolarmente robusto contro attacchi CSRF e altri attacchi di tipo replay.
Se proviamo a omettere il parametro state
o a non verificarlo, riceveremo errori sia da Google che da GitHub, del tipo:
- Google: "Error: Invalid state parameter"
- GitHub: "Bad verification code. The state parameter is invalid"
Le differenze chiave tra i due approcci sono:
-
Gestione dell'Identità:
- OIDC (Google): Fornisce un ID Token (JWT) che contiene informazioni verificabili sull'utente
- OAuth 2.0 (GitHub): Richiede chiamate API aggiuntive per ottenere le informazioni dell'utente
-
Standardizzazione:
- OIDC: Endpoints e flussi di dati standardizzati
- OAuth 2.0: Maggiore flessibilità ma meno standardizzazione
-
Configurazione:
- OIDC: Configurazione automatica tramite discovery document
- OAuth 2.0: Configurazione manuale degli endpoint
-
Scambio di Informazioni:
- OIDC: Un singolo scambio fornisce sia l'autenticazione che le informazioni di base dell'utente
- OAuth 2.0: Richiede scambi multipli (autorizzazione, token, informazioni utente)
Questo dimostra come la sicurezza nel protocollo OAuth 2.0 sia implementata attraverso controlli incrociati e ridondanti, dove sia il provider che il client hanno la responsabilità di mantenere la catena di fiducia.
Questa parte del documento illustra tutti i percorsi implementati nell'applicazione CryptoFolio, comprese le misure di sicurezza, i controlli di accesso e le funzionalità.
/auth/login/<provider>
Parameter | Type | Description |
---|---|---|
provider |
string |
Required. ['Google', 'Github] |
Accesso: Pubblico
Descrizione: Avvia il flusso di autenticazione OAuth per il provider specificato (Google o GitHub).
Questo percorso implementa diverse misure di sicurezza necessarie per la route successiva:
- Generazione di token CSRF senza richiedere l'ID utente
- Convalida del provider per consentire solo “google” e “github”.
- Gestione sicura dei reindirizzamenti
- Gestione dello stato della sessione
Caratteristiche di sicurezza:
- Convalida dell'input per i parametri del provider
- Protezione CSRF attraverso la generazione di token
- Gestione sicura della sessione
- Convalida dei parametri di stato OAuth
Esempio di risposta: Reindirizza alla pagina di login del provider OAuth (Google - Github)
GET /auth/callback/<provider>
Parameter | Type | Description |
---|---|---|
provider |
string |
Required ['Google', 'Github] |
Metodo: GET
Accesso: Riservato
Descrizione: Gestisce il callback OAuth dopo l'autenticazione del provider
Prerequisiti: Token CSRF
Questo percorso implementa diverse misure di sicurezza necessarie per la route successiva:
- Convalida dello stato CSRF
- Crittografia dei dati con AES-256
- Generazione sicura di sale
- Registrazione di tutti i tentativi di autenticazione
- Gestione degli errori con registrazione sicura
Flusso del processo:
-
Convalida lo stato CSRF dal provider
-
Recupera e convalida i token OAuth
-
Recupera le informazioni sull'utente dal provider attraverso il l'authorization token
-
Recupera ID Utente dal provider ed effettua hashing per assicurare coerenza del formato indipendentemente dal provider
def hash_user_id(self, provider: str, original_id: str) -> str: """ Generates a secure and consistent hash of the user ID using HMAC-SHA256, encoded in URL-safe base64 format. Args: provider: OAuth provider identifier (e.g., 'google', 'github') original_id: Original user ID from the provider Returns: str: Base64 encoded hash, URL-safe without padding Raises: ValueError: If provider or original_id is empty CryptographicError: If hashing fails """ if not provider or not original_id: raise ValueError("Provider and user ID are required") try: # Combine provider and ID in a consistent format combined = f"{provider}:{original_id}" # Use HMAC-SHA256 to generate a secure hash hmac_obj = hmac.new( key=self.app_secret.to_bytes(), msg=combined.encode(), digestmod=hashlib.sha256 ) # Convert to URL-safe base64 without padding hash_bytes = hmac_obj.digest() # Get raw bytes instead of hexadecimal return base64.urlsafe_b64encode(hash_bytes).rstrip(b'=').decode('ascii') except Exception as e: self.logger.error(f"Error hashing user ID: {e}") raise CryptographicError("Unable to hash user ID") from e
-
Cifra i dati sensibili dell'utente
-
Crea/aggiorna i record dell'utente
-
Stabilisce una sessione sicura
POST auth/logout
Metodo: POST
Accesso: Riservato ad utenti loggati
Descrizione: Gestisce il processo di logout sicuro dell'utente
Prerequisiti: Token CSRF e Login
Questo percorso implementa diverse misure di sicurezza necessarie:
- Richiede l'autenticazione (login_required)
- Protezione CSRF (csrf.csrf_protect)
- Pulizia completa della sessione
- Registrazione di audit
Risultato: Risposta JSON con redirect alla pagina principale
È bene specificare che tutte queste routes (al di fuori di dashboard), sono accessibili soltanto dalla pagina principale: non è possibile effettuare richieste API esternamente poichè verranno gestite interamente dalla web app, attraverso una firma gemerata da un'origine javascript verificata con una validità limitata attraverso una finestra temporale gestita dal backend.
GET /dashboard
Metodo: GET
Accesso: Riservato ad utenti loggati
Descrizione: Dashboard principale che visualizza i dati crittografati del portfolio con variazioni in tempo reale
Prerequisiti: Token CSRF e Login
Questo percorso implementa diverse misure di sicurezza necessarie
- Autenticazione necessaria
- Cifratura/decifratura dei dati
- Sanificazione dell'input e output
- Gestione degli errori
- Registrazione di audit
Dettagli di implementazione:
- Implementa la paginazione (50 elementi per pagina)
- Logica di ripetizione della connessione (max 3 tentativi)
- Recupero dei prezzi in batch
- Chunking dei dati per le prestazioni
POST /api/portfolio/add
Metodo: POST
Accesso: Riservato ad utenti loggati e accessibile soltanto dalla dashboard
Descrizione: Aggiunge una nuova crypto al portfolio con archiviazione crittografata
Prerequisiti: Token CSRF, Nonce, Ratelimiting, Validazione Origin e Headers, e Login
Questo percorso implementa diverse misure di sicurezza necessarie
- Autenticazione richiesta (login_required)
- Protezione CSRF (csrf.csrf_protect)
- Limitazione della richieste (rate_limit_decorator)
- Sanificazione dell'input e output
- Crittografia dei dati
- Gestione delle transazioni
- Registrazione delle verifiche
Esempio di utilizzo
{
“crypto_id": “bitcoin”,
“symbol": “BTC”,
“amount": 1.5,
“purchase_price": 45000,
“purchase_date": “2024-01-15”
}
PUT /api/portfolio/update/<doc_id>
Metodo: PUT
Accesso: Riservato ad utenti loggati e accessibile soltanto dalla dashboard
Descrizione: Modifica quantità, prezzo di acquisto e data di acquisto di una specifico crypto associata al portfolio con archiviazione crittografata.
Prerequisiti: Token CSRF, Nonce, Login, Validazione Origin e Headers
Campi necessari:
purchase_price
float
Required nuovo prezzo di acquisto
purchase_date
float
Required nuova data di acquisto
purchase_amount
float
Required. nuova quantità di acquisto
Nel caso in cui l'utente decidesse di cambiare solo un valore, i rimanenti valori verranno aggiornati con quelli precedentemente presenti di default.
Questo percorso implementa diverse misure di sicurezza necessarie
- Verifica della proprietà del documento
- Cifratura dei dati
- Sanificazione dell'input
- Registrazione delle verifiche
- Gestione degli errori
DELETE /api/portfolio/delete/<doc_id>
Metodo: DELETE
Accesso: Riservato ad utenti loggati e accessibile soltanto dalla dashboard
Descrizione: Elimina una crypto dal portfolio dell'utente
Prerequisiti: Token CSRF, Nonce, Login, Validazione Origin e Headers
Questo percorso implementa diverse misure di sicurezza necessarie
- Verifica della proprietà dei documenti
- Creazione di un backup crittografato
- Pulizia dei dati associati
- Registrazione di audit
- Gestione degli errori
GET /api/preferences/currency
Metodo: GET
Accesso: Riservato ad utenti loggati e accessibile soltanto attraverso la dashboard
Descrizione: Restituisce la valuta attualmente selezionata dalla dashboard
Prerequisiti: Token CSRF, Nonce, Login, Validazione Origin e Headers
Questo percorso implementa diverse misure di sicurezza necessarie
- Autenticazione necessaria
- Protezione CSRF (csrf.csrf_protect)
PUT /api/preferences/currency
Parameter | Type | Description |
---|---|---|
currency |
string |
Required ['USD', 'EUR' |
Metodo: PUT
Accesso: Riservato ad utenti loggati e accessibile soltanto attraverso la dashboard
Descrizione: Permette di modificare la valuta attualmente selezionata dalla dashboard
Prerequisiti: Token CSRF, Nonce, Login, Validazione Origin e Headers
Questo percorso implementa diverse misure di sicurezza necessarie
- Autenticazione necessaria
- Protezione CSRF (csrf.csrf_protect)
- Sanificazione degli Input
Anche in questo caso, tutte le routes sono accessibili soltanto dalla pagina principale: non è possibile effettuare richieste API esternamente poichè verranno gestite interamente dalla web app, attraverso una firma gemerata da un'origine javascript verificata con una validità limitata.
POST /api/token
Metodo: POST
Accesso: Riservato ad utenti loggati e accessibile soltanto dalla dashboard
Descrizione: Ottieni un token JWT da utilizzare con EndPoint designati all'ottenimento del valore del Portfolio o all'aggiunta di nuove Crypto
Prerequisiti: Token CSRF, Nonce, Login, Validazione Origin e Headers, Prerequisiti per la generazione
Questo percorso implementa diverse misure di sicurezza necessarie
- Autenticazione necessaria
- Protezione CSRF
- Limitazione delle richieste (1 token ogni 12 ore, Max 2 token al giorno)
- Validità del token per 7 giorni
- La generazione di un nuovo token invalida quello precedentemente generato
- Rotazione dei token
- Registrazione di audit
- Tracciamento del dispositivo
GET /api/token/status
Metodo: GET
Accesso: Riservato ad utenti loggati e accessibile soltanto dalla dashboard
Descrizione: Ottieni informazioni sulla validità di un token JWT da utilizzare con EndPoint designati all'ottenimento del valore del Portfolio o all'aggiunta di nuove Crypto
Prerequisiti: Token CSRF, Nonce, Login, Validazione Origin e Headers
Questo percorso implementa diverse misure di sicurezza necessarie
- Autenticazione necessaria
- Protezione CSRF
- Abilità o meno la possibilità di generare un nuovo token
POST /api/token/cleanup
Metodo: POST
Accesso: Riservato ad utenti loggati e accessibile soltanto dalla dashboard
Descrizione: Gestisce i token scaduti modificando la proprietà su Firestore da Valid a Expired
Prerequisiti: Token CSRF, Nonce, Login, Validazione Origin e Headers
Questo percorso implementa diverse misure di sicurezza necessarie
- Autenticazione necessaria
- Protezione CSRF
- Abilità o meno la possibilità di generare un nuovo token JWT
GET api/csrf/token
Metodo: GET
Accesso: Riservato ad utenti loggati e accessibile soltanto attraverso la dashboard
Descrizione: Ottieni un token CSRF da utilizzare per il corretto funzionamento del sistema
Prerequisiti: Nonce, Login, Validazione Origin e Headers
Questo percorso implementa diverse misure di sicurezza necessarie
- Autenticazione necessaria
- Generazione crittograficamente sicura del Token CSRF
- Rotazione dei token
- Eliminazione token CSRF (Protezione DDOS)
GET api/csrf/nonce
Metodo: GET
Accesso: Riservato ad utenti loggati e accessibile soltanto attraverso la dashboard
Descrizione: Ottieni un nonce CSRF da utilizzare per il corretto funzionamento del sistema insieme al Token CSRF
Prerequisiti: Token CSRF, Login, Validazione Origin e Headers
Questo percorso implementa diverse misure di sicurezza necessarie
- Autenticazione necessaria
- Generazione crittograficamente sicura del Nonce CSRF
- Rotazione dei nonce
- Eliminazione nonce scaduti
POST /navigate-home
Metodo: POST
Accesso: Riservato ad utenti loggati e accessibile soltanto dalla dashboard
Descrizione: Riporta l'utente alla pagina di benvenuto
Prerequisiti: Token CSRF, Nonce, , Validazione Origin e Headers, e Login
Questo percorso implementa diverse misure di sicurezza necessarie
- Autenticazione richiesta (login_required)
- Protezione CSRF (csrf.csrf_protect)
GET api/cryptocurrencies
Metodo: GET
Accesso: Riservato ad utenti loggati e accessibile soltanto attraverso la dashboard
Descrizione: Restituisce tutte le crypto disponibili provenienti come risposta dall'API di CoinGecko e le salva nella cache: successivamente i valori verranno prelevati da essa e riaggiornati dopo 30 min.
Prerequisiti: Token CSRF, Nonce, Login, Validazione Origin e Headers
Questo percorso implementa diverse misure di sicurezza necessarie
- Autenticazione necessaria
- Protezione CSRF (csrf.csrf_protect)
Ogni percorso dell'applicazione è protetto da più livelli di sicurezza, secondo il principio della difesa in profondità. Le caratteristiche di sicurezza comuni a tutti i percorsi includono:
- Convalida e sanificazione dell'input
- Gestione degli errori con registrazione sicura
- Limitazione delle richieste, ove appropriato
- Protezione CSRF
- Controlli di autenticazione
- Registrazione di eventi significativi
- Gestione sicura delle sessioni
- Crittografia dei dati per le informazioni sensibili
Tutti i dati di risposta sono accuratamente sanificati per evitare la fuga di informazioni e i messaggi di errore sono generalizzati per evitare di esporre dettagli interni al sistema.
Questa parte specifica della documentazione descrive in dettaglio la configurazione di sicurezza definita e adottata per la Web App, poichè tali parametri offrono protezione contro attacchi comuni, come lo scripting cross-site (XSS) e il clickjacking, e garantiscono che la comunicazione tra il server e il browser avvenga in modo sicuro.
HSTS: str = field(default="max-age=31536000; includeSubDomains")
- Significato:
max-age=31536000
: Impone ai browser di accedere al dominio solo tramite HTTPS per un periodo di 1 anno (31536000 secondi).includeSubDomains
: Estende questa regola a tutti i sottodomini del dominio principale.
- Utilizzo: Garantisce che il traffico del dominio (e dei suoi sottodomini) sia sempre cifrato.
CONTENT_TYPE_OPTIONS: str = field(default="nosniff")
- Significato:
- Previene che il browser interpreti il tipo di contenuto in modo diverso da quanto dichiarato dal server.
- Protegge da attacchi di tipo MIME-sniffing.
- Utilizzo: Blocca il caricamento di contenuti con tipi MIME non validi o non attesi.
FRAME_OPTIONS: str = field(default="DENY")
- Significato:
- Impedisce che il sito venga incorniciato (
<iframe>
) in altri siti web. - Protegge da attacchi di clickjacking.
- Impedisce che il sito venga incorniciato (
- Utilizzo: Blocca qualsiasi tentativo di visualizzare il sito in un iframe.
XSS_PROTECTION: str = field(default="1; mode=block")
- Significato:
- Attiva il filtro XSS del browser.
1
: Abilita il filtro.mode=block
: Blocca completamente la pagina in caso di rilevamento di uno script dannoso.
- Utilizzo: Protezione contro gli attacchi Cross-Site Scripting (XSS).
REFERRER_POLICY: str = field(default="strict-origin-when-cross-origin")
- Significato:
- Controlla quali informazioni del referrer (URL precedente) vengono inviate nelle richieste.
strict-origin-when-cross-origin
:- Invia solo il dominio (
origin
) del referrer per richieste cross-origin. - Invia l'intero referrer solo per richieste verso la stessa origine.
- Invia solo il dominio (
- Utilizzo: Migliora la privacy degli utenti riducendo la quantità di informazioni condivise tra domini.
PERMITTED_CROSS_DOMAIN_POLICIES: str = field(default="none")
- Significato:
- Blocca le richieste di politiche cross-domain come i file
*.swf
o*.xml
.
- Blocca le richieste di politiche cross-domain come i file
- Utilizzo: Previene potenziali abusi relativi a plugin Adobe Flash o Silverlight.
CSP: str = field(default=(
"default-src 'self'; "
"img-src 'self' data: https:; "
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://code.jquery.com; "
"font-src 'self' https://cdnjs.cloudflare.com; "
"connect-src 'self' https://*.ngrok-free.app https://*.ngrok.io; "
"frame-ancestors 'none';"
))
-
default-src 'self';
- Origine predefinita per tutte le risorse non coperte da altre direttive.
'self'
: Consente solo risorse provenienti dallo stesso dominio da cui la pagina è stata servita.
-
img-src 'self' data: https:;
- Controlla le origini consentite per il caricamento delle immagini.
'self'
: Immagini caricate dallo stesso dominio della pagina.data:
: Consente immagini caricate come data URI (es.data:image/png;base64,...
).https:
: Permette immagini da qualsiasi origine che usa HTTPS.
-
style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com;
- Controlla le origini per i fogli di stile (CSS).
'self'
: Fogli di stile dal dominio della pagina.'unsafe-inline'
: Permette stili inline (rischioso, ma utile in alcuni casi specifici).- URL specifici: Permette stili da CDN noti come
jsdelivr
ecdnjs
.
-
script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://code.jquery.com;
- Controlla le origini per il caricamento degli script JavaScript.
'self'
: Solo script dal dominio della pagina.- URL specifici: Consente script da CDN comuni come
jsdelivr
,cdnjs
ecode.jquery.com
.
-
font-src 'self' https://cdnjs.cloudflare.com;
- Origini per i font web.
'self'
: Font dal dominio della pagina.cdnjs.cloudflare.com
: Permette font da questo CDN.
-
connect-src 'self' https://*.ngrok-free.app https://*.ngrok.io;
- Origini per connessioni HTTP o WebSocket.
'self'
: Consente connessioni al proprio dominio.https://*.ngrok-free.app
ehttps://*.ngrok.io
: Permette connessioni a sottodomini dinamici (es. per debug con ngrok).
-
frame-ancestors 'none';
- Specifica chi può incorniciare il sito (usando
<iframe>
). 'none'
: Blocca qualsiasi tentativo di incorniciare il sito.
- Specifica chi può incorniciare il sito (usando
DEFAULT_CORS_MAX_AGE: int = 3600 # 1 hour
- Significato:
- Specifica per quanto tempo una risposta CORS preflight può essere memorizzata nella cache dal browser.
- Utilizzo: Riduce il numero di richieste OPTIONS effettuate dal browser.
DEFAULT_HSTS_MAX_AGE: int = 31536000 # 1 year
- Significato:
- Durata predefinita per l'header HSTS.
- Utilizzo: Fornisce una configurazione standard per la durata delle politiche HSTS.
SUPPORTED_ENVIRONMENTS: Set[str] = frozenset({'development', 'production'})
- Significato:
- Elenco degli ambienti supportati dall'applicazione.
development
: Ambiente di test/sviluppo.production
: Ambiente di produzione con configurazioni più restrittive.
- Utilizzo: Aiuta a gestire configurazioni diverse per ambienti differenti.
cors_headers = {
'Access-Control-Allow-Headers': os.getenv('CORS_ALLOWED_HEADERS', ''),
'Access-Control-Allow-Methods': os.getenv('CORS_ALLOWED_METHODS', ''),
'Access-Control-Allow-Credentials': os.getenv('CORS_ALLOW_CREDENTIALS', ''),
'Access-Control-Expose-Headers': os.getenv('CORS_EXPOSE_HEADERS', ''),
'Access-Control-Max-Age': str(self.DEFAULT_CORS_MAX_AGE)
}
Access-Control-Allow-Headers
: Specifica gli header HTTP che il client può includere nelle richieste cross-origin.Access-Control-Allow-Methods
: Elenca i metodi HTTP consentiti (es. GET, POST, PUT).Access-Control-Allow-Credentials
: Consente al browser di inviare credenziali (es. cookie) nelle richieste cross-origin.Access-Control-Expose-Headers
: Definisce quali header il browser può esporre.Access-Control-Max-Age
: Specifica la durata di caching per le richieste preflight.
security_headers = {
'X-Content-Type-Options': self._security_headers.CONTENT_TYPE_OPTIONS,
'X-Frame-Options': self._security_headers.FRAME_OPTIONS,
'X-XSS-Protection': self._security_headers.XSS_PROTECTION,
'Referrer-Policy': self._security_headers.REFERRER_POLICY,
'X-Permitted-Cross-Domain-Policies': self._security_headers.PERMITTED_CROSS_DOMAIN_POLICIES,
'Content-Security-Policy': self._security_headers.CSP
}
X-Content-Type-Options
: Previene il MIME-sniffing.X-Frame-Options
: Impedisce il framing della pagina.X-XSS-Protection
: Protegge da attacchi XSS.Referrer-Policy
: Limita i dati referrer inviati nelle richieste.X-Permitted-Cross-Domain-Policies
: Blocca richieste cross-domain non autorizzate.Content-Security-Policy
: Gestisce le origini delle risorse caricate dal browser.
I decorator in Python sono uno strumento potente che permette di modificare o estendere il comportamento di funzioni e metodi in modo pulito e riutilizzabile. Nel contesto della sicurezza delle applicazioni web, i decorator giocano un ruolo fondamentale permettendo di implementare controlli di sicurezza in modo modulare e consistente.
Un decorator è essenzialmente una funzione che prende come input un'altra funzione e ne estende il comportamento senza modificarne il codice sorgente. Questo pattern è particolarmente utile per implementare funzionalità trasversali come:
- Autenticazione e autorizzazione
- Rate limiting
- Protezione CSRF
- Logging e audit
- Gestione delle sessioni
- Validazione degli input
Le classi principali che implementano la logica di sicurezza attraverso i decorator sono:
Gestisce le configurazioni di sicurezza dell'applicazione, incluse:
- Gestione delle origini CORS consentite
- Configurazione degli header di sicurezza
- Validazione dell'ambiente
- Protezione contro attacchi di tipo injection
class SecureConfig:
def __init__(self):
self._setup_secure_logging()
self._init_crypto()
self._request_history = {}
self._initialized = False
Implementa la protezione contro attacchi Cross-Site Request Forgery attraverso:
- Generazione e validazione di token
- Gestione dei nonce
- Verifica dell'origine delle richieste
- Tracciamento delle catene di richieste
class CSRFProtection:
def __init__(self, app=None):
self._signing_key = secrets.token_bytes(32)
self.encryption_key = Fernet.generate_key()
self._token_cache = {}
Gestisce l'intero ciclo di vita dei token JWT:
- Generazione sicura dei token
- Validazione e rinnovo
- Gestione della scadenza
Implementa il rate limiting distribuito usando Firebase:
- Tracciamento delle richieste
- Applicazione dei limiti
- Gestione delle finestre temporali
- Cleanup automatico
Questo decorator garantisce che solo gli utenti autenticati possano accedere alle route protette. È implementato come segue:
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
Funzionalità di sicurezza:
- Verifica della presenza di una sessione valida
- Redirect sicuro per utenti non autenticati
- Mantenimento della catena di redirect
- Protezione contro accessi non autorizzati
Implementa il controllo del rate limiting per prevenire abusi delle API e rallentamenti del sistema. I
def rate_limit_decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
user_id = session.get('user_id')
ip_address = request.remote_addr
rate_limiter = FirebaseRateLimiter(db)
is_allowed, remaining, retry_after = rate_limiter.check_rate_limit(
user_id, ip_address)
l sistema di rate limiting utilizza un approccio a doppio livello che combina limitazioni basate sull'IP e sull'identità dell'utente. Questo permette di proteggere l'applicazione sia da attacchi distribuiti che da abusi da parte di singoli utenti autenticati.
Il sistema mantiene le seguenti informazioni per ogni IP:
- Un contatore di richieste
- Il timestamp di inizio della finestra temporale
- Il timestamp dell'ultima richiesta
Per ogni nuova richiesta, il sistema:
- Verifica se esiste già un record per l'IP
- Controlla se la finestra temporale corrente è scaduta
- Verifica se il numero di richieste ha superato il limite
- Aggiorna i contatori in modo atomico usando una transazione Firebase
I limiti utente vengono gestiti separatamente ma in modo analogo ai limiti IP. Questo permette di:
- Avere limiti diversi per utenti autenticati
- Tracciare l'utilizzo per utente indipendentemente dall'IP
- Applicare politiche diverse per utenti specifici
Tutti questi dati vengono gestiti attraverso una finestra temporale:
- Ogni finestra ha una durata configurabile (default: 3600 secondi per IP, configurabile per utente)
- Quando una finestra scade, viene creata una nuova finestra con contatore azzerato
- Le richieste vengono conteggiate all'interno della finestra corrente
- Il sistema mantiene il timestamp di inizio finestra per calcolare quando resettare i contatori
L'utilizzo di Firebase Firestore permette una gestione distribuita del rate limiting:
- Le transazioni atomiche garantiscono la consistenza dei contatori
- I dati sono sincronizzati tra tutte le istanze dell'applicazione
- Il cleanup automatico rimuove i record scaduti
- Il sistema scala automaticamente con il carico
Il sistema aggiunge header di risposta per informare i client:
response.headers['X-RateLimit-Remaining'] = str(remaining)
response.headers['X-RateLimit-Reset'] = str(retry_after)
Questi header permettono ai client di:
- Conoscere il numero di richieste rimanenti
- Sapere quando i limiti verranno resettati
- Implementare logiche di backoff quando necessario
Il sistema implementa un cleanup probabilistico dei dati:
def maybe_cleanup(self):
if random.random() < self.cleanup_probability:
try:
self.cleaner.clean_expired_entries(self.window_seconds)
except Exception as e:
logging.warning(f"Inline cleanup error: {str(e)}")
Questo assicura che:
- I record scaduti vengano rimossi periodicamente
- Il carico del cleanup sia distribuito nel tempo
- Il database non cresca indefinitamente
- Le performance rimangano costanti nel tempo
Fornisce protezione CSRF completa attraverso:
def csrf_protect(self, f):
@wraps(f)
def decorated_function(*args, **kwargs):
nonce = request.headers.get('X-CSRF-Nonce')
if not nonce or not self.validate_nonce(nonce):
abort(403, "Invalid CSRF nonce")
La protezione CSRF (Cross-Site Request Forgery) implementata nell'applicazione utilizza un approccio a più livelli che combina token, nonce e validazione dell'origine. Questo crea una difesa in profondità contro attacchi CSRF sofisticati.
La strategia protettiva dei sistemi informatici denominata Defense in Depth o DiD (difesa in profondità) consiste in una stratificazione delle risorse informatiche di protezione. Il concetto di Difesa in Profondità si origina come strategia militare, per il rallentamento dell’avanzare nemico tramite barriere fisiche.
Agendo in tal modo, era possibile preparare l’effettivo contrattacco, delineando una strategia d’azione coerente e i mezzi attraverso cui attuarla.
Il decorator csrf_protect è il punto di ingresso principale per la protezione CSRF. Ecco come funziona nel dettaglio:
def csrf_protect(self, f):
@wraps(f)
def decorated_function(*args, **kwargs):
# 1. Validazione del nonce
nonce = request.headers.get('X-CSRF-Nonce')
if not nonce or not self.validate_nonce(nonce):
abort(403, "Invalid CSRF nonce")
# 2. Validazione del token
token = request.headers.get('X-CSRF-Token')
if not token or not self.validate_token_request(token):
abort(403, "Invalid CSRF token")
# 3. Validazione dell'origine
origin = request.headers.get('Origin')
if origin and not self._validate_origin_secure(origin):
abort(403, "Invalid request origin")
return f(*args, **kwargs)
return decorated_function
Il sistema utilizza nonce monouso per prevenire gli attacchi replay:
def validate_nonce(self, nonce: str) -> bool:
try:
if not nonce or nonce not in self.used_nonces:
return False
nonce_data = self.used_nonces[nonce]
current_time = time.time()
# Verifica scadenza
if current_time > nonce_data['expires']:
del self.used_nonces[nonce]
return False
# Decrittazione e validazione payload
payload = json.loads(
self.fernet.decrypt(nonce.encode()).decode()
)
# Verifica binding con l'utente
if payload['user_id'] != session.get('user_id'):
return False
# Rimozione dopo l'uso (one-time use)
del self.used_nonces[nonce]
return True
except Exception as e:
self.logger.error(f"Errore validazione nonce: {str(e)}")
return False
Il nonce fornisce:
- Protezione contro attacchi replay
- Binding con la sessione utente
- Scadenza temporale automatica
- Utilizzo singolo garantito
Il sistema implementa una validazione completa del token CSRF:
def validate_token_request(self, token: str) -> bool:
if not token:
return False
cookie_token = request.cookies.get('csrf_token')
# Verifica che entrambi i token siano presenti e corrispondano
if not cookie_token or cookie_token != token:
self.logger.warning("Token mismatch tra cookie e header")
return False
# Validazione origine
origin = request.headers.get('Origin')
if origin:
if not self._validate_origin_format(origin):
self.logger.warning(f"Formato origine non valido: {origin}")
return False
if not self._check_origin_allowed(origin):
self.logger.warning(f"Origine non consentita: {origin}")
return False
# Validazione referrer per richieste same-origin
referrer = request.headers.get('Referer')
if referrer:
ref_url = urlparse(referrer)
req_url = urlparse(request.url)
if ref_url.netloc != req_url.netloc:
self.logger.warning(f"Referrer non valido: {referrer}")
return False
return self._validate_token(token)
La validazione del token include:
- Controllo del formato e della firma
- Controllo del token presente nel cookie
- Validazione dell'origine della richiesta
- Verifica del referrer
- Binding con la sessione utente
I token vengono generati in modo sicuro con multiple protezioni:
def _generate_secure_token(self, require_user_id=True) -> str:
if require_user_id and 'user_id' not in session:
abort(401)
# Cleanup dei token scaduti
self._cleanup_expired_tokens()
if (require_user_id):
# Validazione origine JavaScript
js_origin = request.headers.get('X-JavaScript-Origin')
if not js_origin or not self._validate_js_origin(js_origin):
abort(403, "Invalid request origin")
user_id = session['user_id']
# Generazione componenti token
timestamp = int(time.time())
random_bytes = secrets.token_bytes(32)
request_id = secrets.token_hex(16)
# Creazione payload
payload = {
'user_id': user_id,
'timestamp': timestamp,
'request_id': request_id,
'random': base64.b64encode(random_bytes).decode()
}
# Cifratura payload
encrypted_payload = self.fernet.encrypt(
json.dumps(payload).encode()
)
# Generazione firma HMAC
signature = hmac.new(
self._signing_key,
encrypted_payload,
hashlib.sha256
).digest()
# Composizione token finale
token = base64.urlsafe_b64encode(
encrypted_payload + signature
).decode()
# Salvataggio in cache con metadata
self._token_cache[user_id][token] = {
'timestamp': timestamp,
'uses': 0,
'request_id': request_id
}
return token
La generazione include:
- Entropia crittografica tramite secrets
- Crittografia del payload con Fernet
- Firma HMAC per integrità
- Caching sicuro con metadata
Il sistema implementa una validazione completa dell'origine delle richieste:
def _validate_origin_secure(self, origin: str) -> bool:
if not origin or '\x00' in origin:
return False
try:
parsed = urlparse(origin)
# Validazione protocollo
if parsed.scheme not in {'http', 'https'}:
return False
# Gestione origini locali
is_local = (
parsed.netloc.startswith('localhost') or
parsed.netloc.startswith('127.0.0.1') or
parsed.netloc == '[::1]'
)
if is_local:
if os.getenv('FLASK_ENV') != 'development':
return False
# Validazione porta per sviluppo locale
if ':' in parsed.netloc:
port = int(parsed.netloc.split(':')[1])
if not (1024 <= port <= 65535):
return False
return True
# Validazione dominio
if not parsed.netloc or '.' not in parsed.netloc:
return False
return True
except Exception as e:
self.logger.error(f"Errore validazione origine: {str(e)}")
return False
La validazione dell'origine assicura:
- Formato URL valido
- Protocollo consentito
- Domini autorizzati
- Porte consentite in sviluppo
Il sistema gestisce in modo sicuro l'intero ciclo di vita dei token:
def _cleanup_expired_tokens(self) -> None:
current_time = time.time()
for user_id, tokens in list(self._token_cache.items()):
# Rimuovi token scaduti o sovra-utilizzati
valid_tokens = {}
for token, data in tokens.items():
is_valid = (
(current_time - data['timestamp']) <= self._token_lifetime and
data['uses'] < self._max_uses_per_token
)
# Gestione speciale token di autenticazione
if data.get('is_auth_flow', False):
is_valid = is_valid and data['uses'] == 0
if is_valid:
valid_tokens[token] = data
if valid_tokens:
self._token_cache[user_id] = valid_tokens
else:
del self._token_cache[user_id]
# Protezione DoS - Se troppi token, rimuovi i più vecchi
total_tokens = sum(len(tokens)
for tokens in self._token_cache.values())
if total_tokens > self._max_tokens_per_session * len(self._token_cache):
for user_id in self._token_cache:
tokens = self._token_cache[user_id]
if len(tokens) > self._max_tokens_per_session:
sorted_tokens = sorted(
tokens.items(),
key=lambda x: x[1]['timestamp'],
reverse=True
)
self._token_cache[user_id] = dict(
sorted_tokens[:self._max_tokens_per_session]
)
Il ciclo di vita include:
- Pulizia automatica token scaduti
- Limiti di utilizzo per token
- Protezione contro accumulo token
- Gestione speciale token di autenticazione
Il sistema di protezione CSRF implementa una difesa in profondità attraverso:
-
Validazione multipla delle richieste
- Token CSRF
- Nonce monouso
- Controllo origine
- Validazione referrer
-
Gestione sicura dei token
- Generazione crittografica
- Cifratura payload
- Firme HMAC
- Cleanup automatico
-
Protezione sessione
- Cookie sicuri
- Binding utente
- Timeout automatico
- Limiti di utilizzo
-
Prevenzione attacchi
- Anti-replay
- Anti-DoS
- Anti-timing
- Sanificazione input
La combinazione di questi meccanismi crea più livelli di protezione, rendendo estremamente difficile bypassare la sicurezza anche se un singolo controllo viene compromesso.
- Valida l'età della sessione
- Forza la ri-autenticazione per sessioni scadute
- Traccia l'ultima attività
- Implementa timeout di inattività di 60 minuti
@app.before_request
def check_session_timeout():
if 'last_active' in session:
last_active = datetime.fromtimestamp(session['last_active'])
if datetime.now() - last_active > timedelta(minutes=60):
session.clear()
Aggiunge header di sicurezza a tutte le risposte (precedentemente analizzato):
- Configurazione CORS
- Content Security Policy
- HSTS enforcement
- Protezione XSS
- Prevenzione Clickjacking
Nell'applicazione, il sale crittografico viene utilizzato principalmente per tre scopi fondamentali:
- Protezione delle informazioni sensibili degli utenti
- Generazione di identificatori utente univoci e sicuri
- Derivazione delle chiavi di crittografia
Quando un nuovo utente si registra attraverso OAuth, viene generato un sale univoco:
# Nel callback OAuth
secure_salt = cipher.generate_salt()
security_ref = db.collection('user_security').document(user_id)
# Conversione del SecureByteArray in stringa base64 per storage
salt_bytes = secure_salt.to_bytes()
encoded_salt = base64.b64encode(salt_bytes).decode()
# Memorizzazione nel database
security_ref.set({
'salt': encoded_salt,
'created_at': firestore.SERVER_TIMESTAMP,
'last_login': firestore.SERVER_TIMESTAMP,
'oauth_token_metadata': token_metadata
})
Il metodo generate_salt() nella classe AESCipher produce un sale crittograficamente sicuro:
def generate_salt(self) -> SecureByteArray:
try:
return SecureByteArray(os.urandom(self.SALT_LENGTH))
except Exception as e:
self.logger.error(f"Error generating salt: {e}")
raise CryptographicError("Unable to generate salt")
Il sale viene utilizzato per la derivazione delle chiavi di crittografia quando si devono cifrare dati sensibili dell'utente:
def derive_key(self, user_id: str, salt: Union[bytes, SecureByteArray]) -> SecureByteArray:
key_material = None
secure_salt = None
derived_key = None
try:
# Conversione sicura del sale
secure_salt = self._ensure_secure_bytes(salt)
# Configurazione KDF (Key Derivation Function)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA3_256(),
length=self.KEY_LENGTH,
salt=secure_salt.to_bytes(),
iterations=self.KDF_ITERATIONS,
backend=default_backend()
)
# Combinazione sicura di user_id e master_key
key_material = SecureByteArray(
user_id.encode() + self.master_key.to_bytes()
)
# Derivazione della chiave
derived_key = SecureByteArray(kdf.derive(key_material.to_bytes()))
return derived_key
finally:
# Pulizia sicura della memoria
for secure_data in [key_material, secure_salt]:
if secure_data is not None and secure_data is not salt:
secure_data.secure_zero()
Quando si gestiscono le informazioni del portfolio dell'utente, il sale viene utilizzato per cifrare i dati sensibili:
# Recupero del sale dell'utente per operazioni di portfolio
security_ref = db.collection('user_security').document(user_id)
security_data = security_ref.get()
encoded_salt = security_data.to_dict()['salt']
salt_bytes = base64.b64decode(encoded_salt)
# Cifratura di un elemento del portfolio
encrypted_data = portfolio_encryption.encrypt_portfolio_item(
validated_data,
user_id,
salt_bytes
)
# Decifratura di un elemento del portfolio
decrypted_item = portfolio_encryption.decrypt_portfolio_item(
encrypted_item,
user_id,
salt_bytes
)
Gli indirizzi email degli utenti vengono cifrati usando il sale dell'utente prima di essere memorizzati:
# Durante la registrazione dell'utente
encrypted_email = cipher.encrypt(
user_email,
user_id,
secure_salt
).decode()
user_ref.set({
'username': username,
'email': encrypted_email,
'preferred_currency': 'USD',
'created_at': firestore.SERVER_TIMESTAMP,
'last_login': firestore.SERVER_TIMESTAMP,
'provider': provider
})
L'applicazione utilizza la classe SecureByteArray per gestire in modo sicuro il sale in memoria:
class SecureByteArray:
def secure_zero(self) -> None:
"""Cancella in modo sicuro i dati dalla memoria"""
if self._length == 0:
return
try:
for _ in range(self.SECURE_WIPE_PASSES):
# Sovrascrittura con dati casuali
random_data = secrets.token_bytes(
max(self._length, self.MIN_RANDOM_BYTES)
)
ctypes.memmove(self._address, random_data, self._length)
# Passaggio finale di azzeramento
ctypes.memset(self._address, 0, self._length)
except Exception as e:
self.logger.error(f"Errore durante la pulizia sicura della memoria: {e}")
raise MemorySecurityError(
"Impossibile cancellare la memoria in modo sicuro"
)
L'utilizzo del sale nell'applicazione fornisce diversi benefici di sicurezza:
-
Unicità per Utente: Ogni utente ha un sale univoco, quindi anche se due utenti hanno gli stessi dati, i valori cifrati saranno diversi.
-
Protezione contro Attacchi Rainbow Table: Il sale rende inefficaci gli attacchi basati su tabelle precalcolate.
-
Isolamento dei Dati: Il sale per utente garantisce che una compromissione dei dati di un utente non comprometta gli altri.
-
Derivazione Sicura delle Chiavi: Il sale permette di derivare chiavi di cifratura uniche per ogni utente partendo dalla master key.
Nel contesto dello sviluppo di applicazioni web moderne, la necessità di esporre ambienti di sviluppo locali a internet in modo sicuro rappresenta una sfida cruciale. Questa esigenza diventa particolarmente rilevante quando si sviluppano funzionalità che richiedono interazioni con servizi esterni, come l'autenticazione OAuth o le callback API di terze parti. Il tunneling sicuro con ngrok offre una soluzione elegante a questa sfida, creando un ponte crittografato tra l'ambiente di sviluppo locale e internet. Questo approccio non solo facilita il testing e lo sviluppo, ma implementa anche robuste misure di sicurezza per proteggere le comunicazioni.
Come illustrato nel diagramma, l'architettura del sistema di tunneling si compone di tre elementi principali: Ngrok crea una connessione sicura tra l'endpoint pubblico e il computer locale, in modo che il traffico sia crittografato e sicuro. Quando la richiesta arriva all'endpoint pubblico, viene crittografata e inoltrata al server locale. Ngrok utilizza una combinazione di crittografia e tecniche di instradamento della rete per creare una connessione sicura e affidabile tra l'endpoint pubblico e il computer locale. In questo modo Ngrok consente di eseguire facilmente il tunnel del server locale, garantendo al contempo la sicurezza.
Client Esterno: Rappresenta qualsiasi client che tenta di accedere all'applicazione attraverso internet.
Ngrok Cloud: Agisce come intermediario sicuro, gestendo la terminazione TLS e la validazione delle origini.
Applicazione Flask Locale: L'ambiente di sviluppo che viene esposto in modo sicuro.
Il flusso delle richieste attraversa questi componenti mediante tunnel crittografati, garantendo la sicurezza end-to-end delle comunicazioni. Il sistema implementa una rigorosa crittografia TLS per tutto il traffico che attraversa il tunnel. Anche quando l'applicazione locale opera su HTTP, ngrok forza l'utilizzo di HTTPS per tutte le connessioni esterne. Questo garantisce che ogni bit di dati trasmesso rimanga confidenziale e integro.
Un aspetto critico della sicurezza è la validazione delle origini delle richieste. Il sistema implementa una complessa catena di verifiche che include:
- Validazione delle firme crittografiche per le richieste JavaScript
- Verifica dei timestamp per prevenire replay attack
- Controlli specifici per i domini ngrok in ambiente di sviluppo
Il sistema di tunneling sicuro con ngrok rappresenta un esempio eccellente di come sia possibile bilanciare la necessità di accessibilità durante lo sviluppo con rigorosi requisiti di sicurezza.
CryptoFolio fornisce un'API RESTful che permette di gestire il proprio portfolio di criptovalute in modo programmatico e sicuro. Questa documentazione descrive come utilizzare le API disponibili per interagire con il tuo portfolio.
Tutte le richieste API devono essere autenticate utilizzando un token JWT (JSON Web Token). Per ottenere il token:
- Accedi alla dashboard di CryptoFolio
- Vai alla sezione "API Access"
- Clicca su "Generate New Token"
Il token ha una validità di 7 giorni e deve essere incluso nell'header Authorization
di ogni richiesta nel seguente formato:
RICORDA: Se pensi che qualcuno sia venuto a conoscenza del tuo token JWT, potrai rinnovarlo nelle 12 ore immediatamente successive alla generazione, accedendo alla tua dashboard. Il token precedentemente generato verrà correttamente invalidato dal sistema.
Authorization: Bearer il_tuo_token_jwt
Secondo RFC6750 The OAuth 2.0 Authorization Framework Il Bearer Token è un token di sicurezza che soltanto le parti autorizzati possono utilizzare, da qui il nome *Bearer, ovvero un possessore. Lato server, questo Token è prodotto attraverso lo standard JWT.
L'aggiunta del termine “Bearer” prima del token nell'intestazione “Authorization” ha due scopi importanti:
- Identificazione: La parola chiave “Bearer” aiuta il server a identificare facilmente il tipo di token utilizzato e a gestirlo in modo appropriato durante i processi di autenticazione e autorizzazione.
- Standardizzazione: L'uso dello schema “Bearer” è una convenzione ampiamente adottata e una pratica raccomandata per chiarezza e standardizzazione. Promuove l'interoperabilità tra i diversi sistemi e componenti coinvolti nel flusso di autenticazione, riducendo le possibilità di interpretazioni o comunicazioni errate.
Sebbene tecnicamente sia possibile eseguire l'autenticazione senza includere esplicitamente la parola chiave “Bearer”, si raccomanda vivamente di includerla per un'autenticazione corretta utilizzando lo schema del token Bearer. L'aggiunta di “Bearer” prima del token garantisce chiarezza, coerenza e compatibilità tra le diverse implementazioni e i diversi sistemi.
Quando il server riceve una richiesta HTTP con l'intestazione “Authorization”, controlla la presenza della parola chiave “Bearer” per determinare lo schema di autenticazione utilizzato. Senza la parola chiave “Bearer”, il server potrebbe non riconoscere il token come token Bearer e potrebbe non riuscire ad autenticare o autorizzare correttamente la richiesta.
Pertanto, è importante includere sempre la parola chiave “Bearer” prima dell'operazione.
Lo standard JWT rappresenta semplicemente un formato di serializzazione di informazioni (claim), espressi in JSON (JavaScript Object Notation).
Ogni JWT è composto (a parte casi particolari) da tre parti, codificate in base64:
- Header
- Payload
- Signature
Per il calcolo della firma, è stato utilizzato HS256 algoritmo simmetrico, nel quale la stessa chiave è usata per generare e validare la firma.
Quindi è bene specificare che i Bearer Token sono un tipo particolare di Access Token, usati per ottenere l'autorizzazione ad accedere ad una risorsa protetta da un Authorization Server, mentre il JWT è un formato di serializzazione.
Endpoint: GET /api/v1/portfolio
Questo endpoint restituisce l'elenco completo delle criptovalute nel tuo portfolio.
Esempio di richiesta:
curl -X GET \
'https://api.cryptofolio.com/api/v1/portfolio' \
-H 'Authorization: Bearer il_tuo_token_jwt'
Esempio di risposta:
{
"status": "success",
"data": [
{
"crypto_id": "bitcoin",
"symbol": "BTC",
"amount": 0.5,
"purchase_price": 45000,
"purchase_date": "2025-01-15",
"current_price": 48000,
"current_value": 24000,
"profit_loss": 1500,
"profit_loss_percentage": 6.25
},
// altri elementi del portfolio...
],
"total_value": 24000,
"currency": "USD"
}
Endpoint: POST /api/v1/portfolio
Questo endpoint permette di aggiungere una nuova criptovaluta al tuo portfolio.
Parametri richiesti:
crypto_id
(string): Identificativo della criptovaluta (es. "bitcoin")symbol
(string): Simbolo della criptovaluta (es. "BTC")amount
(number): Quantità acquistatapurchase_price
(number): Prezzo di acquisto in USDpurchase_date
(string): Data di acquisto in formato "YYYY-MM-DD"
Esempio di richiesta:
curl -X POST \
'https://api.cryptofolio.com/api/v1/portfolio' \
-H 'Authorization: Bearer il_tuo_token_jwt' \
-H 'Content-Type: application/json' \
-d '{
"crypto_id": "bitcoin",
"symbol": "BTC",
"amount": 0.5,
"purchase_price": 45000,
"purchase_date": "2025-01-15"
}'
Esempio di risposta:
{
"status": "success",
"message": "Cryptocurrency added successfully"
}
{
"crypto_id": "bitcoin",
"symbol": "BTC",
"amount": 0.5,
"purchase_price": 42000.00,
"purchase_date": "2024-01-15"
}
{
"crypto_id": "ethereum",
"symbol": "ETH",
"amount": 2.0
// Error
}
{
"crypto_id": "ripple",
"symbol": "XRP",
"amount": "invalid",
"purchase_price": -100,
"purchase_date": "2024-01-15"
// Error
}
{
"crypto_id": "cardano",
"symbol": "ADA",
"amount": 1000,
"purchase_price": 0.50,
"purchase_date": "invalid-date"
// Error
}
{
"crypto_id": "dogecoin<script>",
"symbol": "DOGE';--",
"amount": 1000,
"purchase_price": 0.10,
"purchase_date": "2024-01-15"
// Error
}
Per garantire un servizio ottimale, sono in vigore i seguenti limiti:
- Massimo 100 richieste all'ora condivise tra i due EndPoint
- Massimo 2 token generabili al giorno
- Periodo di attesa di 12 ore tra una generazione di token e l'altra
- Validità di 7 giorni per ogni Token
L'API utilizza i codici di stato HTTP standard e restituisce gli errori nel seguente formato:
{
"status": "error",
"message": "Descrizione dell'errore"
}
Codici di stato comuni:
400
: Richiesta non valida (dati mancanti o formato errato)401
: Token di autenticazione mancante o non valido429
: Superato il limite di richieste500
: Errore interno del server
- Non condividere mai il tuo token JWT
- Memorizza il token in modo sicuro (es. variabili d'ambiente)
- Rigenera periodicamente il token per maggiore sicurezza
- Non includere mai il token nel codice sorgente
- Utilizza sempre HTTPS per le richieste
D: Come posso sapere se il mio token sta per scadere?
R: Il token include una data di scadenza che puoi verificare nella dashboard. Ti consigliamo di rinnovare il token qualche giorno prima della scadenza per evitare interruzioni del servizio.
D: Cosa succede se supero il limite di richieste?
R: Riceverai un errore 429 con un header Retry-After
che indica dopo quanti secondi potrai riprovare.
D: È possibile utilizzare valute diverse da USD?
R: Al momento i prezzi sono forniti solo in USD. La conversione in altre valute deve essere gestita lato client. È necessario cambiare la valuta dalla dashboard per poter ricevere il controvalore correttamente attraverso l'Api.
Prima di installare CryptoFolio, assicurati di avere installato quanto segue:
- Python 3.7 o superiore
- pip (gestore di pacchetti Python)
- Git
- Un account Firebase
- Un account Google Cloud Platform (per OAuth)
- Un account sviluppatore GitHub (per OAuth)**
- Clona il repository
git clone https://github.com/GabrielCellammare/CryptoFolio-App.git
cd CryptoFolio-App
- Crea e attiva un ambiente virtuale
python -m venv venv
source venv/bin/activate # Su Windows: venv\Scripts\activate
- Installa le dipendenze
pip install -r requirements.txt
- Modifica il file
.env(example)
nella directory principale e configura tutte le variabili:
# Ambiente
FLASK_ENV=development
FLASK_RUN_PORT=5000
NGROK_AUTH_TOKEN=your_ngrok_token # Opzionale per lo sviluppo
NGROK_REGION=eu
# Origini (Sviluppo)
DEV_ALLOWED_ORIGINS=http://localhost:5000,http://127.0.0.1:5000,https://*.ngrok.io,https://*.ngrok-free.app
PROD_ALLOWED_ORIGINS=https://yourdomain.com
# Configurazione CORS
CORS_MAX_AGE=86400
CORS_ALLOWED_HEADERS=Content-Type, X-CSRF-Token, X-CSRF-Nonce, X-Requested-With, X-Client-Version
CORS_ALLOWED_METHODS=GET, POST, PUT, DELETE, OPTIONS
CORS_ALLOW_CREDENTIALS=true
CORS_EXPOSE_HEADERS=Content-Type
# Impostazioni di Sicurezza
FLASK_SECRET_KEY=your_generated_64_char_secret_key
MASTER_ENCRYPTION_KEY=your_generated_64_char_encryption_key
HASH_SECRET_KEY=your_generated_64_char_hash_key
JWT_SECRET_KEY=your_generated_256_char_jwt_key
# Configurazione OAuth
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
SERVER_METADATA_URL_GOOGLE=https://accounts.google.com/.well-known/openid-configuration
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
# Chiavi API
COINGECKO_API_KEY=your_coingecko_api_key
# Configurazione della Sessione
SESSION_TIMEOUT_MINUTES=60
Genera chiavi sicure per le variabili d'ambiente:
import secrets
# Genera FLASK_SECRET_KEY (64 caratteri)
print(secrets.token_urlsafe(48))
# Genera MASTER_ENCRYPTION_KEY (64 caratteri)
print(secrets.token_urlsafe(48))
# Genera HASH_SECRET_KEY (64 caratteri)
print(secrets.token_hex(32))
# Genera JWT_SECRET_KEY (256 caratteri)
print(secrets.token_hex(128))
- Vai al Google Cloud Console
- Crea un nuovo progetto o seleziona uno esistente
- Abilita l'API Google e la schermata di consenso OAuth
- Configura la schermata di consenso OAuth:
- Aggiungi la tua email per il contatto sviluppatore
- Aggiungi domini autorizzati
- Aggiungi ambiti per email e profilo
- Crea un ID Client OAuth 2.0:
- Aggiungi origini JavaScript autorizzate:
http://localhost:5000
https://*.ngrok-free.app
- Aggiungi URI di reindirizzamento autorizzati:
http://localhost:5000/auth/callback/google
https://*.ngrok-free.app/auth/callback/google
- Copia l'ID Client e il Segreto Client nel tuo file
.env
- Vai alle Impostazioni Sviluppatore GitHub
- Clicca su "New OAuth App"
- Configura l'applicazione:
- Nome dell'applicazione: CryptoFolio
- URL della homepage:
http://localhost:5000
(sviluppo) o il tuo URL di produzione - URL di callback per l'autorizzazione:
http://localhost:5000/auth/callback/github
(sviluppo)https://https://*.ngrok-free.app/auth/callback/github
- Copia l'ID Client e il Segreto Client nel tuo file
.env
Durante ogni nuovo avvio dell'applicazione, sarà necessario configurare URL di callback per l'autorizzazione di Google e di Github con il link prodotto da ngrok.
- Crea un nuovo progetto Firebase su Firebase Console
- Abilita Firestore Database
- Configura le regole di sicurezza di Firestore:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Profili utente
match /users/{userId} {
allow read: if request.auth.uid == userId;
allow write: if request.auth.uid == userId;
// Sottocollezione del portafoglio
match /portfolio/{portfolioId} {
allow read: if request.auth.uid == userId;
allow write: if request.auth.uid == userId;
}
}
// Dati di sicurezza utente
match /user_security/{userId} {
allow read: if request.auth.uid == userId;
allow write: if request.auth.uid == userId;
}
// Log di audit
match /audit_logs/{logId} {
allow read: if false;
allow write: if false;
}
}
}
- Genera una chiave privata Firebase Admin SDK:
- Vai a Impostazioni Progetto > Account di Servizio
- Clicca su "Genera Nuova Chiave Privata"
- Salva il file JSON come
firebase_config.json
nella radice del progetto (vedi esempio)
- Iscriviti a un account CoinGecko
- Genera una chiave API dalla tua dashboard
- Aggiungi la chiave al tuo file
.env
comeCOINGECKO_API_KEY
Assicurati che le seguenti directory esistano nella radice del tuo progetto (verranno create automaticamente dal sistema):
mkdir cache
mkdir instance
mkdir logs
chmod 700 cache instance logs # Proteggi i permessi delle directory
L'applicazione creerà automaticamente le collezioni Firestore necessarie al primo avvio:
- users
- user_security
- audit_logs
- error_logs
- rate_limits
- deleted_portfolios
export FLASK_ENV=development
flask run
- Non committare mai il file
.env
ofirebase_config.json
nel controllo di versione - Ruotate regolarmente le chiavi di sicurezza e i token API
- Monitorare i log di audit per attività sospette
- Mantenere tutte le dipendenze aggiornate
- Eseguire regolarmente il backup dei dati di Firestore
- Usare HTTPS in produzione
- Errori di Callback OAuth
- Verificare che gli URL di callback corrispondano esattamente a quelli nelle impostazioni del provider OAuth
- Controllare il protocollo corretto (http vs https)
- Assicurarsi che tutti gli ambiti richiesti siano abilitati
- Problemi di Connessione Firebase
- Verificare che
firebase_config.json
sia formattato correttamente - Controllare le impostazioni del progetto Firebase
- Verificare la connettività di rete e le regole firewall
- Errori di Sessione
- Controllare che FLASK_SECRET_KEY sia impostato correttamente
- Verificare le impostazioni dei cookie di sessione
- Controllare la configurazione CORS corretta
- Controllare i log dell'applicazione nella directory
logs
- Rivedere i log della Console Firebase
- Controllare la collezione dei log di audit in Firestore
- Rivedere la collezione error_logs per informazioni dettagliate sugli errori
Attività di manutenzione regolare:
- Monitorare e ruotare le chiavi API
- Aggiornare le dipendenze
- Rivedere e analizzare i log di audit
- Pulire i token scaduti
- Monitorare l'efficacia del rate limiting
- Rivedere e aggiornare le regole di sicurezza