Come abilitare CORS con HTTPOnly Cookie per Secure Token?

Abilitare CORS con cookie HTTPOnly per la protezione dei token

In questo articolo, esploreremo come implementare CORS (Cross-Origin Resource Sharing) utilizzando cookie HTTPOnly, al fine di proteggere efficacemente i token di accesso.

Oggi, le architetture web prevedono spesso server back-end e client front-end distribuiti su domini separati. Pertanto, è fondamentale che il server abiliti CORS per permettere ai client di comunicare correttamente via browser.

Inoltre, molti server adottano un approccio di autenticazione stateless, che favorisce la scalabilità. In questo modello, i token sono memorizzati e gestiti lato client, a differenza delle sessioni, che sono gestite lato server. Per una maggiore sicurezza, è consigliabile conservare questi token all’interno di cookie HTTPOnly.

Perché le richieste cross-origin sono bloccate dai browser?

Consideriamo un esempio: la nostra applicazione frontend è ospitata su https://app.winadmin.it.com. Uno script caricato su questo dominio può di norma accedere solamente alle risorse che condividono la stessa origine.

Se tentiamo di inviare una richiesta cross-origin verso un dominio differente come https://api.winadmin.it.com, o verso una porta diversa come https://app.winadmin.it.com:3000, o persino con un protocollo differente, la richiesta verrà bloccata dal browser.

È interessante notare come la stessa richiesta, bloccata dal browser, venga invece elaborata senza problemi da un server back-end utilizzando curl o da strumenti come Postman. Questa differenza è dovuta al meccanismo di sicurezza implementato nei browser per prevenire attacchi come il CSRF (Cross-Site Request Forgery).

Immaginiamo che un utente sia autenticato nel proprio account PayPal. Se fosse possibile inviare una richiesta cross-origin a paypal.com da uno script eseguito su un dominio malevolo (es. malicious.com) senza alcun controllo CORS, un aggressore potrebbe sfruttare questa vulnerabilità.

Ad esempio, un aggressore potrebbe creare una pagina malevola all’indirizzo https://malicious.com/transfer-money-to-attacker-account-from-user-paypal-account, e nasconderla dietro un URL accorciato. Un utente che cliccasse su questo link, senza saperlo, darebbe il via all’esecuzione di uno script che trasferirebbe denaro dal suo account PayPal a quello dell’aggressore. Questo tipo di attacco potrebbe colpire tutti gli utenti PayPal autenticati che dovessero cliccare sul link, senza che se ne rendano conto.

Per i suddetti motivi, i browser bloccano le richieste cross-origin per impostazione predefinita.

Cos’è CORS (Cross-Origin Resource Sharing)?

CORS è un meccanismo di sicurezza basato su intestazioni che i server utilizzano per comunicare ai browser quali domini sono considerati attendibili per effettuare richieste cross-origin. L’attivazione di CORS sul server permette di evitare il blocco delle richieste cross-origin da parte dei browser.

Come funziona CORS?

Il server definisce i propri domini attendibili nella configurazione CORS. Quando un client invia una richiesta al server, la risposta includerà informazioni che indicano al browser se il dominio che ha effettuato la richiesta è considerato attendibile o meno.

Esistono due tipi di richieste CORS:

  • Richiesta semplice
  • Richiesta di pre-volo

Richiesta semplice:

  • Il browser invia una richiesta a un dominio cross-origin con la sua origine (es. https://app.winadmin.it.com).
  • Il server risponde fornendo informazioni sui metodi e le origini consentite.
  • Il browser confronta l’origine della richiesta con l’intestazione `Access-Control-Allow-Origin` ricevuta dal server. Se corrispondono o se l’intestazione contiene un carattere jolly, la richiesta è considerata valida. Altrimenti, viene generato un errore CORS.

  • Richiesta preliminare:
  • Se una richiesta cross-origin prevede l’utilizzo di metodi specifici (PUT, DELETE), intestazioni personalizzate, o un tipo di contenuto differente, il browser invia una richiesta preliminare OPTIONS per verificare se la richiesta effettiva è sicura.

Se la risposta alla richiesta OPTIONS ha un codice di stato 204 (nessun contenuto) e i parametri sono validi, allora la richiesta cross-origin effettiva viene inviata ed elaborata.

Se `access-control-allow-origin: *`, la risposta è consentita da qualsiasi dominio. Ma non è sicuro a meno che non sia necessario.

Come abilitare CORS?

Per abilitare CORS per un dominio specifico, è necessario configurare le seguenti intestazioni CORS sul server:

  • Il browser legge le intestazioni CORS dal server e permette le richieste del client solo se i parametri corrispondono.
  • `Access-Control-Allow-Origin`: specifica i domini autorizzati (es. `https://app.geekflate.com`, `https://lab.winadmin.it.com`) o utilizza un carattere jolly (`*`).
  • `Access-Control-Allow-Methods`: permette l’utilizzo di specifici metodi HTTP (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS).
  • `Access-Control-Allow-Headers`: definisce quali intestazioni personalizzate sono consentite (es. `Autorizzazione`, `token csrf`).
  • `Access-Control-Allow-Credentials`: valore booleano che indica se sono ammesse credenziali cross-origin (cookie, intestazioni di autorizzazione).

`Access-Control-Max-Age`: comunica al browser per quanto tempo può memorizzare la risposta di preflight nella cache.

`Access-Control-Expose-Headers`: specifica quali intestazioni sono accessibili agli script lato client.

Per attivare CORS su server Apache e Nginx, si consiglia di consultare guide specifiche.

    const express = require('express');
    const app = express()

    app.get('/users', function (req, res, next) {
      res.json({msg: 'user get'})
    });

    app.post('/users', function (req, res, next) {
        res.json({msg: 'user create'})
    });

    app.put('/users', function (req, res, next) {
        res.json({msg: 'User update'})
    });

    app.listen(80, function () {
      console.log('CORS-enabled web server listening on port 80')
    })
    

Abilitare CORS in ExpressJS

Un esempio di app ExpressJS senza CORS:

npm install cors

Nell’esempio, l’API degli utenti accetta metodi POST, PUT, GET ma non DELETE.

Per semplificare l’abilitazione di CORS in ExpressJS, si può utilizzare il middleware `cors`:

    app.use(cors({
        origin: '*'
    }));
  

`Access-Control-Allow-Origin`

    app.use(cors({
    origin: 'https://app.winadmin.it.com'
    }));
    

Abilitazione CORS per tutti i domini

    app.use(cors({
        origin: [
            'https://app.geekflare.com',
            'https://lab.geekflare.com'
        ]
    }));
  

Abilitazione di CORS per un singolo dominio

Per permettere l’accesso da https://app.winadmin.it.com e https://lab.winadmin.it.com:

    app.use(cors({
        origin: [
            'https://app.geekflare.com',
            'https://lab.geekflare.com'
        ],
        methods: ['GET', 'PUT', 'POST']
    }));
    

Metodi di controllo accessi

Per abilitare tutti i metodi omettere questa opzione. Altrimenti, specificare i metodi autorizzati (GET, POST, PUT):

    app.use(cors({
        origin: [
            'https://app.geekflare.com',
            'https://lab.geekflare.com'
        ],
        methods: ['GET', 'PUT', 'POST'],
        allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token']
    }));
    

`Access-Control-Allow-Headers`

Serve per permettere l’invio di intestazioni non standard con le richieste:

    app.use(cors({
        origin: [
            'https://app.geekflare.com',
            'https://lab.geekflare.com'
        ],
        methods: ['GET', 'PUT', 'POST'],
        allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
        credentials: true
    }));
    

`Access-Control-Allow-Credentials`

Se non si vuole consentire l’invio di credenziali nelle richieste, anche se `withCredentials` è impostato su `true`, omettere questa opzione.

    app.use(cors({
        origin: [
            'https://app.geekflare.com',
            'https://lab.geekflare.com'
        ],
        methods: ['GET', 'PUT', 'POST'],
        allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
        credentials: true,
        maxAge: 600
    }));
  

`Access-Control-Max-Età`

Indica al browser per quanto tempo conservare le informazioni sulla risposta di preflight. Omettere se non si desidera usare la cache.

        app.use(cors({
            origin: [
                'https://app.geekflare.com',
                'https://lab.geekflare.com'
            ],
            methods: ['GET', 'PUT', 'POST'],
            allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
            credentials: true,
            maxAge: 600,
            exposedHeaders: ['Content-Range', 'X-Content-Range']
        }));
  

La risposta memorizzata nella cache sarà disponibile per 10 minuti.

        app.use(cors({
            origin: [
                'https://app.geekflare.com',
                'https://lab.geekflare.com'
            ],
            methods: ['GET', 'PUT', 'POST'],
            allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
            credentials: true,
            maxAge: 600,
            exposedHeaders: ['*', 'Authorization', ]
        }));
  

`Access-Control-Expose-Headers`

Se si usa il carattere jolly (`*`) in `ExposedHeaders`, l’intestazione di autorizzazione non verrà esposta. Bisogna specificarla esplicitamente.

L’esempio precedente esporrà tutte le intestazioni, inclusa quella di autorizzazione.

  • Cos’è un cookie HTTP?
  • Un cookie è un piccolo dato che il server invia al browser. Nelle richieste successive, il browser invierà tutti i cookie pertinenti al dominio.
  • Un cookie ha diversi attributi che ne influenzano il comportamento:
  • `Nome`: il nome del cookie.
  • `Valore`: i dati del cookie associati al nome.
  • `Dominio`: il dominio a cui verranno inviati i cookie.
  • `Percorso`: i cookie saranno inviati solo per URL che iniziano con il percorso specificato. Ad esempio, se impostato su `admin/`, i cookie non saranno inviati a `https://winadmin.it.com/expire/`, ma a `https://winadmin.it.com/admin/`.
  • `Max-Età/Scade`: (numero in secondi): indica quando il cookie scade e diventa non valido.
  • `HTTPOnly` (booleano): se impostato su `true`, il cookie può essere letto solo dal server, non da script lato client.
  • `Sicuro` (booleano): se impostato su `true`, il cookie è inviato solo su connessioni HTTPS.
  • `stessoSito` (stringa): controlla se i cookie devono essere inviati nelle richieste cross-site. Per maggiori dettagli sui cookie `stessoSito` si rimanda alla documentazione MDN. Accetta tre valori: `Strict`, `Lax`, `None`. Per l’utilizzo con `sameSite=None` è necessario impostare anche il parametro `Sicuro` a `true`.

Perché utilizzare cookie HTTPOnly per i token?

Memorizzare il token di accesso lato client (localStorage, indexedDB, cookie non HTTPOnly) è più vulnerabile agli attacchi XSS. Se una pagina è vulnerabile a un attacco XSS, gli aggressori possono rubare i token utente dal browser.

I cookie HTTPOnly possono essere letti solo dal server.

  • Gli script lato client non possono accedere a questo tipo di cookie. Quindi, i cookie HTTPOnly non sono vulnerabili agli attacchi XSS e sono più sicuri, poiché accessibili solo dal server.
  • Per abilitare i cookie HTTPOnly con CORS, è necessaria la seguente configurazione lato server:
  • Impostare l’intestazione `Access-Control-Allow-Credentials` su `true`.

`Access-Control-Allow-Origin` e `Access-Control-Allow-Headers` non devono essere caratteri jolly (`*`).

    const express = require('express');
    const app = express();
    const cors = require('cors');

    app.use(cors({
      origin: [
        'https://app.geekflare.com',
        'https://lab.geekflare.com'
      ],
      methods: ['GET', 'PUT', 'POST'],
      allowedHeaders: ['Content-Type', 'Authorization', 'x-csrf-token'],
      credentials: true,
      maxAge: 600,
      exposedHeaders: ['*', 'Authorization' ]
    }));

    app.post('/login', function (req, res, next) {
      res.cookie('access_token', access_token, {
        expires: new Date(Date.now() + (3600 * 1000 * 24 * 180 * 1)), //second min hour days year
        secure: true, // set to true if your using https or samesite is none
        httpOnly: true, // backend only
        sameSite: 'none' // set to none for cross-request
      });

      res.json({ msg: 'Login Successfully', access_token });
    });

    app.listen(80, function () {
      console.log('CORS-enabled web server listening on port 80')
    });
    

L’attributo `sameSite` del cookie deve essere impostato su `None`.

Per abilitare `sameSite` a `none`, impostare anche il valore `secure` a `true`, per comunicare al browser che il server utilizza un certificato SSL/TLS sul nome di dominio.

Vediamo un esempio di codice che imposta il token di accesso in un cookie HTTPOnly dopo la verifica delle credenziali di login.

Seguendo i quattro passi descritti, è possibile configurare correttamente i cookie CORS e HTTPOnly.

      var xhr = new XMLHttpRequest();
      xhr.open('GET', 'https://api.winadmin.it.com/user', true);
      xhr.withCredentials = true;
      xhr.send(null);
      

Si consiglia di consultare guide specifiche per la configurazione di CORS su Apache e Nginx.

      fetch('https://api.winadmin.it.com/user', {
        credentials: 'include'
      });
      

`withCredentials` per richieste cross-origin

        $.ajax({
            url: 'https://api.winadmin.it.com/user',
            xhrFields: {
                withCredentials: true
            }
        });
        

Le credenziali (cookie, autorizzazione) sono inviate di default nelle richieste della stessa origine. Per le richieste cross-origin è necessario impostare `withCredentials` su `true`.

axios.defaults.withCredentials = true

API di richiesta XMLHttp

Recupera API

JQuery Ajax

Spero che questo articolo abbia chiarito come funziona CORS e come abilitarlo per le richieste cross-origin. Abbiamo anche visto perché i cookie HTTPOnly sono sicuri e come utilizzare `withCredentials` nel client per le richieste cross-origin.