CodeGym /Corsi /ChatGPT Apps /Controllo degli accessi e minimizzazione dei privilegi: s...

Controllo degli accessi e minimizzazione dei privilegi: scope, segmentazione, autorizzazioni per strumento (per‑tool permissions)

ChatGPT Apps
Livello 15 , Lezione 0
Disponibile

1. Perché pensare ai permessi in una ChatGPT‑App (e qual è il rischio specifico)

In un’applicazione web «normale» tra l’utente e il vostro database ci sono solo un paio di layer: frontend, API, DB. In una ChatGPT‑App tra l’utente e le API appare un altro attore attivo — la LLM. E non è un semplice «filtro testuale», ma un’entità che:

  • sceglie autonomamente quali strumenti invocare e con quali argomenti;
  • può essere ingannata da un’iniezione di prompt nei dati;
  • può «confondere» gli strumenti o inventare argomenti che non vi aspettavate.

Se date alla LLM troppi privilegi, ottenete il classico problema Confused Deputy: il modello esegue in buona fede ciò che, a suo parere, chiede l’utente o il testo nei documenti, ma nel frattempo invoca delete_all_orders invece di get_last_order.

Quindi il nostro obiettivo:

  1. Minimizzare i privilegi degli auth_token (quali dati e azioni sono accessibili in assoluto).
  2. Limitare quali strumenti siano disponibili al modello nello specifico scenario.
  3. Aggiungere controllo umano dove le conseguenze sono particolarmente critiche.

E tutto ciò senza scadere nella paranoia e nel blocco totale, altrimenti l’App diventa inutile. Trovare il giusto equilibrio tra usabilità e sicurezza è la nostra missione principale in questo modulo.

2. Modello di accesso nell’ecosistema: chi accede a cosa

Per non perdersi, guardiamo il sistema nel suo insieme. Abbiamo diversi livelli, ciascuno con la propria area di responsabilità e i propri privilegi.

flowchart TD
  U[Utente in ChatGPT] --> C[ChatGPT UI + LLM]
  C --> A["La tua App (piano visivo + widget)"]
  A --> G[MCP Gateway / API Edge]
  G --> S[Server MCP e microservizi]
  S --> D[Database, code, API esterni]

Ruoli in breve:

  • ChatGPT UI e LLM: sono gestiti da OpenAI. Voi fornite loro istruzioni (system‑prompt, descrizioni degli strumenti), ma non controllate i token interni né i privilegi della piattaforma.
  • La vostra App (piano, tools, widget): decidete quali strumenti sono disponibili, come sono descritti, quali conferme UX servono, quali dati il widget può mostrare.
  • MCP Gateway / API Edge: qui avviene la verifica del token, il mapping di userId, tenantId, l’elenco degli scopes e l’instradamento verso il servizio giusto.
  • Server MCP e microservizi: eseguono gli strumenti, fanno richieste al DB e alle API esterne. Qui i controlli devono essere il più rigorosi possibile: scopes, isolamento per tenant, validazione degli input.
  • Storage e API esterni: ultima linea di difesa (limitazioni a livello di DB, privilegi degli account dei servizi esterni).

Idea chiave: la LLM non è una fonte di autorizzazioni. Tutto ciò che arriva al server MCP va trattato come «richiesta dell’utente, formulata dal modello». Decidere se l’operazione sia davvero consentita è responsabilità del vostro codice backend, non del prompt.

3. AuthN vs AuthZ: che cosa facciamo già e cosa aggiungiamo

Nel modulo sull’autenticazione avete già fatto:

  • AuthN (Authentication) — avete stabilito chi è l’utente. Tramite OAuth 2.1/PKCE ChatGPT otteneva dall’IdP un token che veniva poi allegato alle chiamate MCP. In esso c’erano sub, user_id o simili, talvolta tenant_id.
  • AuthZ di base — forse avevate già separato i ruoli user/admin e verificato almeno «è un utente» o «è un admin».

Ora complichiamo il quadro:

  • ogni auth_token deve portare con sé un insieme di scope — diritti in forma di stringa tipo resource:action, per esempio catalog:read, orders:write, payments:create;
  • il vostro server MCP deve verificare la corrispondenza di questi scopes per ogni azione, non solo «una volta all’ingresso»;
  • strumenti diversi e persino operazioni diverse all’interno dello stesso strumento possono richiedere scope differenti.

Nei termini di OAuth 2.1 ChatGPT è un «public client», MCP è un «resource server», e il vostro server OAuth conosce quali scope sono supportati e che cosa significano. I metadati della risorsa MCP in genere dichiarano scopes_supported, in modo che ChatGPT possa richiedere all’utente esattamente i permessi necessari.

4. Progettiamo gli scope per GiftGenius

Prendiamo il nostro GiftGenius didattico e vediamo quali domini di dati e azioni ha. A livello funzionale, qualcosa come:

  • visualizzazione del catalogo e delle schede regalo;
  • raccomandazioni basate sulla cronologia;
  • creazione degli ordini;
  • avvio del checkout / addebito;
  • modifica del catalogo lato amministrazione.

Invece di creare un unico onnipotente giftgenius:full_access, è meglio scomporre in scope ragionevoli.

Convenzione sui nomi: resource:action

Funziona bene la strategia resource:action, dove:

  • resource descrive il dominio: catalog, recommendations, orders, payments, admin.
  • action descrive il tipo di azione: read, write, talvolta più specifico: create, delete, manage.

Esempio per GiftGenius:

Scope Cosa consente
catalog:read
Leggere il catalogo pubblico dei regali
recommendations:read
Leggere la cronologia delle raccomandazioni dell’utente
orders:write
Creare nuovi ordini
orders:read
Leggere la cronologia ordini dell’utente
payments:create
Avviare un pagamento / checkout
catalog:admin
Modificare il catalogo (solo per UI admin/supporto)

Un utente normale di GiftGenius richiederà qualcosa come (separati da spazi): catalog:read recommendations:read orders:write orders:read payments:create. All’amministratore aggiungiamo catalog:admin.

Importante: non creare uno universale *:* o admin:all. Quanto più granulare, tanto più semplice revocare un diritto specifico senza rompere l’intera applicazione.

Tipi di scope: read vs write vs critical

È utile classificare mentalmente gli scope in categorie:

  • sicuri (read): non cambiano lo stato, al massimo rivelano dati;
  • mutanti (write): creano/modificano entità, incrementano contatori, ma non toccano denaro e non effettuano cancellazioni massicce;
  • critici (critical): pagamenti, eliminazione dell’account, cancellazione massiva dei dati.

Per i privilegi critici si possono applicare controlli rafforzati:

  • assegnarli al numero minimo di utenti;
  • richiedere all’utente un consenso separato nella UI di ChatGPT al momento dell’emissione del token;
  • lato MCP, richiedere un’ulteriore conferma (ad esempio un PIN monouso, ma sono scenari avanzati).

Scope nel codice: RequestContext e requireScope

A livello MCP è comodo definire un tipo di contesto unico:

// mcp/context.ts
export interface RequestContext {
  userId: string;        // chi
  tenantId: string;      // nell’ambito di quale organizzazione
  scopes: string[];      // quali privilegi sono stati concessi al token
}

// Semplice helper per la verifica dei privilegi
export function requireScope(
  ctx: RequestContext,
  needed: string
) {
  if (!ctx.scopes.includes(needed)) {
    throw new Error(`Missing scope: ${needed}`);
  }
}

Si presume che RequestContext lo formiate nel MCP Gateway dopo la validazione del token: decodificate il JWT, verificate firma/scadenza, estraete sub, tenant, scope — e poi allegate questo contesto a tutte le chiamate degli strumenti.

Poi, nel tool‑handler:

// mcp/tools/createOrder.ts
import { requireScope, RequestContext } from "../context";

export async function createOrder(
  input: CreateOrderInput,
  ctx: RequestContext
) {
  requireScope(ctx, "orders:write");
  // poi: logica di creazione dell’ordine
}

Ora, anche se il modello invoca improvvisamente createOrder dove a livello di UX non ve lo aspettavate, senza orders:write lo strumento semplicemente non verrà eseguito.

securitySchemes a livello di strumento

La specifica MCP consente a ciascuno strumento di indicare quali schemi di autorizzazione e quali scope sono necessari. Negli esempi ufficiali securitySchemes è collegato direttamente alla descrizione dello strumento.

Esempio ipotetico:

// mcp/server.ts
server.registerTool(
  "createOrder",
  {
    title: "Create order",
    description: "Creates a new order for current user",
    inputSchema: {/*...*/},
    securitySchemes: [
      { type: "oauth2", scopes: ["orders:write"] }
    ]
  },
  async ({ input }, ctx: RequestContext) => {
    requireScope(ctx, "orders:write");
    // ...
  }
);

Qui ci sono due livelli di protezione:

  • dichiarativo: ChatGPT sa che per questo strumento serve orders:write e, in mancanza dei permessi, avvierà il flusso di autenticazione (o lo comunicherà all’utente);
  • imperativo: il vostro codice verifica nuovamente tutto prima dell’azione reale.

Se il token c’è, ma mancano gli scope, il server deve restituire un errore con WWW-Authenticate: Bearer error="insufficient_scope", scope="orders:write" — e ChatGPT potrà richiedere all’utente l’ampliamento dei permessi (autorizzazione step‑up).

Insight

Negli esempi ufficiali si usa securitySchemes. Tuttavia, non è stata approvata nella specifica ufficiale nel formato in cui è scritta negli esempi del ChatGPT Apps SDK. Perciò va contrassegnata come estensione del protocollo ufficiale — incapsulandola in _meta. Una versione funzionante dell’esempio sopra:

// mcp/server.ts
server.registerTool(
  "createOrder",
  {
    title: "Create order",
    description: "Creates a new order for current user",
    inputSchema: {/*...*/},
    _meta: {										// così
      securitySchemes: [
        { type: "oauth2", scopes: ["orders:write"] }
      ]          
    }
  },
  async ({ input }, ctx: RequestContext) => {
    requireScope(ctx, "orders:write");
    // ...
  }
);

5. Autorizzazioni per strumento (per‑tool permissions) e strumenti «pericolosi»

Gli scope rispondono alla domanda «che cosa, in linea di principio, può fare questo auth_token». Ma nel token c’è anche l’elenco degli strumenti che il modello può usare. Anche quelli vanno progettati con attenzione.

Classificazione degli strumenti

Dividiamo gli strumenti in modo approssimativo in:

  • informativi (informational / read‑only): leggono dati, costruiscono report, effettuano calcoli senza effetti collaterali;
  • attuativi (consequential): modificano lo stato, addebitano denaro, cancellano qualcosa.

La documentazione di ChatGPT Apps raccomanda esplicitamente di segnalare come sicuri gli strumenti read‑only e, per quelli pericolosi, di descrivere le conseguenze e includere conferme UX aggiuntive.

Questo si può fare:

  • tramite annotazioni sullo strumento (campi come readOnlyHint, destructiveHint);
  • tramite una descrizione testuale: «Questo strumento elimina ordini in modo irreversibile»;
  • tramite un flag separato confirmation_required, che il piano della vostra App usa per inserire uno step di conferma nel dialogo.

Conferme UX per azioni critiche

Per esempio, GiftGenius ha uno strumento chargeCustomer (avvia un addebito). Non volete certo che il modello lo invochi senza il consenso dell’utente.

Come potrebbe apparire a livello di piano dell’App:

// app/plan/tools.ts (pseudocodice)
export const tools = [
  {
    name: "giftgenius.list_catalog",
    description: "Mostra il catalogo dei regali",
    annotations: { readOnlyHint: true }
  },
  {
    name: "giftgenius.create_order",
    description: "Crea un ordine senza pagamento",
    annotations: { consequential: true }
  },
  {
    name: "giftgenius.charge_customer",
    description: "Addebita l'importo per l'ordine",
    annotations: {
      consequential: true,
      destructiveHint: true,
      confirmationRequired: {
        title: "Addebitare l'importo sulla carta?",
        message: "Verrà effettuato un pagamento per l’ordine N."
      }
    }
  }
];

I nomi esatti dei campi dipendono dalla versione dell’SDK, ma l’idea coincide con le raccomandazioni: gli strumenti read‑only sono marcati come sicuri, quelli pericolosi come bisognosi di conferma esplicita e di una buona spiegazione nella descrizione.

Poi il vostro widget può reagire: se il modello propone di chiamare charge_customer, mostrate all’utente una modale con una formulazione chiara e solo dopo il clic su «Conferma» effettuate realmente la chiamata allo strumento.

Esempio di componente nel widget (semplificato):

// widget/components/ConfirmCharge.tsx
export function ConfirmCharge(props: {
  orderId: string;
  onConfirm: () => void;
}) {
  return (
    <div>
      <p>Addebitare l’importo per l’ordine {props.orderId}?</p>
      <button onClick={props.onConfirm}>
        Sì, conferma il pagamento
      </button>
    </div>
  );
}

Il modello propone l’idea «è ora di pagare», ma il pulsante finale lo preme la persona. Questo è il human‑in‑the‑loop, tanto apprezzato dai team di sicurezza.

Strumenti solo per agenti/back office

Un altro caso comune: avete strumenti che possono usare solo gli agenti (nell’accezione di Agents SDK) o le admin interne, non il «normale» ChatGPT App dell’utente.

Per esempio, rebuildSearchIndex o syncCatalogFromERP. Per questi è meglio:

  • non includerli nella lista generale dei tools per l’App utente;
  • configurarli in un agente/orchestratore separato;
  • proteggerli con scope separati e, possibilmente, con un perimetro Auth separato.

Se li aggiungete semplicemente alla lista degli strumenti disponibili nell’App, aumentate il rischio che il modello decida all’improvviso: «Rifaccio l’indice proprio adesso, magari aiuta a trovare il regalo».

6. Segmentazione di rete e confini di fiducia

I permessi non sono solo gli scopes nel token. Il secondo grande asse è la segmentazione della rete e dei servizi.

Scenario ideale:

  • avete un solo ingresso pubblico verso il backend — MCP Gateway/Edge API;
  • tutto ciò che conserva PII e denaro vive in una rete privata/VPC ed è accessibile solo attraverso questo gateway;
  • il traffico in uscita dal backend è limitato a un elenco di domini consentiti (allowlist: provider di pagamenti, CRM, i vostri microservizi).

Schematicamente:

flowchart LR
  ChatGPT -- HTTPS --> Edge[API Gateway / MCP Endpoint]
  Edge -- private network --> MCP[MCP server]
  MCP -- private --> DB[(DB con PII)]
  MCP -- private --> SVC[Internal microservices]
  MCP -- HTTPS (allow) --> Stripe[Payments API]

Qui sono importanti alcune regole:

  1. DB e servizi interni non sono esposti direttamente a Internet. Accesso diretto solo dalla rete privata e solo dai servizi che ne hanno davvero bisogno.
  2. Edge/Gateway esegue auth e rate‑limiting. È qui che si verifica il token e gli scope, si limitano le richieste troppo frequenti e si scrivono i log di audit principali.
  3. Controllo dell’egress. Il server MCP non dovrebbe poter chiamare qualsiasi URL su Internet (attacchi SSRF, esfiltrazione dati). Meglio limitare esplicitamente l’elenco degli host esterni.

In pratica, se effettuate il deploy di MCP su Vercel, Render o in un cluster Kubernetes, alcune di queste cose non si configurano manualmente, ma anche lì si può separare:

  • progetti/cluster distinti per dev/staging/prod;
  • variabili d’ambiente e chiavi diverse per ciascun ambiente;
  • un servizio «edge» separato (wrapper HTTP di MCP) e servizi privati separati.

In sintesi, abbiamo già due assi di protezione: permessi sul token (scopes) e confini di rete. Aggiungiamone un altro — la multitenancy, quando la stessa App serve più organizzazioni.

7. Multi‑tenant / contesto organizzativo

Finora abbiamo ragionato su un singolo utente. Ma molte applicazioni ChatGPT sono multi‑tenant: la stessa App serve decine di aziende. GiftGenius si può facilmente trasformare in un servizio B2B per aziende: ogni reparto con i propri cataloghi, budget, ordini.

Che cos’è il tenant e dove lo si ottiene

Per tenant si intende di solito:

  • un’organizzazione/azienda (Acme Corp);
  • uno spazio di lavoro (workspace);
  • talvolta un progetto o un ambiente.

Proprietà principale: i dati di un tenant non devono essere visibili a un altro.

Nel flusso di auth il tenant in genere viene messo in:

  • un claim del token (tenant, org_id);
  • un parametro separato nella richiesta di autorizzazione (ma è meno affidabile di un claim firmato dall’IdP).

Importante: ci fidiamo solo del tenantId dal token verificato, non dagli argomenti degli strumenti. Se il modello genera {"tenantId": "acme"}, mentre nel token dell’utente tenantId: "globex", questo va considerato come un tentativo di violazione.

Tenant nel contesto della richiesta

Aggiungiamo tenantId al nostro RequestContext (lo abbiamo già fatto sopra) e non consentiamo di sovrascriverlo dagli input.

Controllo di base:

// mcp/tenant.ts
import { RequestContext } from "./context";

export function enforceTenant<TInput>(
  input: TInput & { tenantId?: string },
  ctx: RequestContext
) {
  if (input.tenantId && input.tenantId !== ctx.tenantId) {
    throw new Error("Tenant mismatch");
  }
  return { ...input, tenantId: ctx.tenantId };
}

Poi nello strumento:

// mcp/tools/listOrders.ts
export async function listOrders(
  input: { limit?: number; tenantId?: string },
  ctx: RequestContext
) {
  const safe = enforceTenant(input, ctx);
  return db.order.findMany({
    where: { tenantId: safe.tenantId },
    take: safe.limit ?? 20
  });
}

Ignoriamo il tenant dagli argomenti e lo imponiamo rigidamente dal contesto. Così, anche se la LLM o un aggressore prova a «infilare» un tenant altrui, non funzionerà.

Isolamento tenant a livello di DB

A livello architetturale ci sono diverse opzioni:

  • DB separato per tenant;
  • schemi separati;
  • un solo DB con tenant_id in ogni tabella e un filtro rigoroso.

Qualunque opzione scegliate, la regola d’oro è una: nessuna query al DB deve essere eseguita senza filtrare per tenant_id dal contesto. È particolarmente importante nel RAG/ricerca vettoriale: se si dimentica il filtro per tenant, il modello può iniziare a cercare nei documenti di altre organizzazioni.

8. Come si collega al nostro progetto Next.js/Apps SDK

Mettiamo ora insieme tutto questo e vediamo come scope, tenant e confini di rete si concretizzano nel nostro progetto Next.js basato su Apps SDK. Aggiungiamo più concretezza e guardiamo il codice di Next.js e dell’Apps SDK.

Dove vivono scope e tenant nel nostro progetto

Tipicamente, per un progetto didattico:

  • Nell’applicazione Next.js (Apps SDK) avete la configurazione dell’App/connettore e le pagine per le callback OAuth.
  • Nel server MCP — il codice che riceve richieste HTTP/SSE da ChatGPT, verifica il token e invoca lo strumento necessario.

Portiamo lì tutto quanto abbiamo discusso:

  1. Nelle impostazioni OAuth della risorsa MCP dichiariamo scopes_supported per GiftGenius (catalog:read, orders:write, ecc.).
  2. Nella configurazione dell’Apps SDK descriviamo l’App con l’elenco degli strumenti e le loro annotazioni (read‑only, consequential, confirmation‑flows).
  3. Nel server MCP implementiamo:
    • parsing e verifica del token;
    • formazione del RequestContext { userId, tenantId, scopes };
    • helper requireScope, enforceTenant, ecc.;
    • le chiamate al DB sempre con il tenantId dal contesto.

Esempio di percorso «isolato» per la creazione di un ordine

Proviamo a seguire uno scenario end‑to‑end.

  1. L’utente scrive: «Effettua un ordine per questo set con un budget di 50 $».
  2. Il modello decide che deve invocare giftgenius.create_order con argomenti { productId, budget, ... }.
  3. ChatGPT verifica: l’App ha lo strumento create_order? Quali scope e securitySchemes sono indicati per esso? Capisce che serve orders:write.
  4. Se il token esiste già e contiene orders:write, la richiesta procede; altrimenti ChatGPT avvia l’autorizzazione OAuth richiedendo lo scope necessario.
  5. MCP Gateway riceve la richiesta, verifica il token, forma il RequestContext con userId=123, tenantId="acme", scopes=["catalog:read","orders:write",...].
  6. createOrder dentro MCP:
    • esegue requireScope(ctx, "orders:write");
    • tramite enforceTenant fissa il tenant;
    • crea l’ordine solo nell’ambito di tenantId="acme".
  7. Se l’ordine richiede pagamento immediato, il modello o il backend avviano quindi charge_customer, dove:
    • lo strumento nel piano è marcato come confirmationRequired;
    • il widget renderizza ConfirmCharge e chiede all’utente di confermare esplicitamente l’addebito.

Così otteniamo una difesa in profondità: prompt troppo ampi, iniezioni di prompt o persino bug nella UX non porteranno ad azioni incontrollate, perché alla base rimangono controlli rigorosi di scope, tenant e conferme manuali per le azioni critiche.

9. Errori tipici nella progettazione dei permessi e della segmentazione

Errore n. 1: Uno scope «onnicomprensivo» come app:full_access.
Questo approccio è comodo in demo, ma pericoloso in produzione. Perdete un token — perdete tutto. Non è possibile revocare o vietare una singola operazione senza rompere le altre. Suddividete i privilegi per dominio e tipo di operazione (read/write/critical).

Errore n. 2: Verificare i permessi solo «all’ingresso» e non dentro gli strumenti.
A volte si fa così: «se ChatGPT ha ottenuto il token, allora può fare tutto». E poi lo strumento createOrder viene semplicemente chiamato, anche se per quel token non era stato concesso orders:write. L’approccio corretto è verificare gli scope in ogni strumento (o almeno con un middleware centralizzato per tutte le operazioni mutanti).

Errore n. 3: Non marcare gli strumenti pericolosi e non richiedere conferma.
Se uno strumento addebita denaro, elimina dati o modifica permessi, non deve apparire al modello come listCatalog. L’assenza di annotazioni esplicite e conferme UX aumenta la probabilità che il modello lo invochi «solo perché sembra logico». Al minimo, separate gli strumenti read‑only da quelli distruttivi e marcate esplicitamente questi ultimi.

Errore n. 4: Fidarsi del tenantId dagli argomenti dello strumento.
Antipattern molto comune: uno strumento getOrders({ tenantId }) in cui il tenantId arriva dal modello. Se lo si usa così com’è, un utente del tenantA può accedere ai dati di tenantB indicando un altro identificatore. Il tenant deve arrivare dal token verificato ed essere imposto a tutte le query al DB e ai servizi esterni; i valori dell’utente vanno ignorati o validati per la corrispondenza.

Errore n. 5: MCP/DB accessibili direttamente da Internet.
A volte, nei prototipi semplici, il server MCP e il DB sono esposti su Internet su HTTP/5432. In produzione non si fa: tutto l’accesso deve passare da un unico gateway/proxy protetto, e il DB deve vivere in una rete privata. Altrimenti qualsiasi endpoint vulnerabile trovato o un webhook mal configurato è una strada diretta ai dati.

Errore n. 6: Usare gli stessi scope/secret in dev e in prod.
Un modo «preferito» per cancellare improvvisamente dati di produzione durante una demo in ambiente di sviluppo locale. Per ogni ambiente devono esserci chiavi, scope e DB propri. Anche se qualcuno ottiene accesso a un token di dev, non potrà danneggiare i dati di prod.

Errore n. 7: Ritrosia a «rifiutare al modello».
A volte gli sviluppatori temono: «Se restituisco spesso errori insufficient_scope o forbidden, il modello funzionerà peggio». In pratica è un comportamento normale e atteso: il modello impara quali azioni sono disponibili e quali richiedono permessi aggiuntivi o conferme. Peggio è quando «riesce» a fare ciò che non dovrebbe — ad esempio, eseguire un secondo pagamento.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION