Vai al contenuto

πŸ†” Resource IDs

Quale problema risolvono?

Un tipico form di modifica incorpora l'ID della risorsa come campo nascosto:

<input type="hidden" name="recno" value="42">

Ma quell'ID viaggia in chiaro. Qualsiasi utente puΓ² ispezionare l'HTML, cambiare il 42 in un 99 e inviare il POST a /customer/edit per modificare un record che non Γ¨ suo. Hai bisogno di:

  1. Ricontrollare i permessi sul 99 lato server (l'approccio giusto, ma costoso e facile da dimenticare).
  2. Firmare l'ID così che il client non possa alterarlo - questa è la soluzione che UResourceToHtml fornisce.
Client riceve    <input ... value="MTAwfDE3MzQ...sig=abc123">  (firmato)
Client invia     _resource_id=MTAwfDE3MzQ...sig=abc123
Server           UGetResource() -> "100"  (valido, accettalo)
Server           se il client altera 1 byte -> UGetResource() -> ""

Il token contiene l'ID + timestamp + HMAC firmato con app_key. Senza conoscere il segreto, l'attaccante non puΓ² generarne uno valido per un ID diverso.


Quando usarlo?

Caso d'uso Resource IDs
Form di edit/delete per un record specifico βœ… SΓ¬
Liste con azioni come <button data-id="42"> βœ… SΓ¬
ID di client / ordine / fattura che viaggia nell'URL ⚠️ No - l'URL è /customer/:id, c'è già il middleware di auth
Token monouso per il download di file βœ… SΓ¬
API REST con JWT ❌ No - il JWT identifica l'utente; convalida l'ownership con query

Non sostituiscono i controlli di permesso. I Resource IDs garantiscono l'integritΓ  dell'ID (non manomesso), non l'autorizzazione. Devi comunque verificare che l'utente corrente possa modificare quello specifico record.


API

Generare - nel controller / template

USetView( "cResourceHtml", UResourceToHtml( nRecno ) )
<form method="POST" action="/customer/edit">
  {{ UCsrfToHtml() }}
  {{ UResourceToHtml( nRecno ) }}
  <input name="first" value="{{ hRow['first'] }}">
  ...
  <button>Salva</button>
</form>

UResourceToHtml( "100" ) genera qualcosa come:

<input type="hidden" name="_resource_id" value="MTAwfDE3MzQ4OTAxMjM=.aBcD3f...">

Validare - nel controller POST

LOCAL cId := UGetResource()        // legge _resource_id da POST -> GET

IF Empty( cId )
   RETURN URedirect( URoute( "main" ) )      // token mancante / non valido
ENDIF

nId := Val( cId )                  // ID originale recuperato

UGetResource() cerca il token automaticamente:

  1. UPost( "_resource_id" ) - prima dal body del POST
  2. UGet( "_resource_id" ) - fallback nella query string
  3. Se non trovato o la firma non Γ¨ valida β†’ ritorna ""

Esempio reale - customer.prg di Fenix

Action Update

METHOD Update() CLASS Customer
   LOCAL cId := UGetResource()                 // β¬… recupera il recno firmato
   LOCAL oVal, nId, cError, lSuccess

   // 1. Abbiamo un token valido?
   IF Empty( cId )
      RETURN URedirect( URoute( "main" ) )     // forgiato / corrotto -> main
   ENDIF

   // 2. L'ID Γ¨ un numero valido?
   oVal := UValidatorOne( "Id", cId, "required|number|min:0" )
   IF oVal:Fails()
      RETURN URedirect( URoute( "customer.search" ) )
   ENDIF
   nId := oVal:Get()

   // 3. Valida il resto del form
   oVal := UValidatePost( { ;
      "first"  => "required|string|max:20|field", ;
      "last"   => "required|string|max:20|field", ;
      "city"   => "required|string|max:30|field", ;
      ... ;
   } )

   IF ! oVal:Make()
      UFlash( "customer" ):Set( { ;
         "type"   => "danger",      ;
         "errors" => oVal:GetErrors(), ;
         "input"  => oVal:Resume() } )
      RETURN URedirect( URoute( "customer.edit", nId ) )
   ENDIF

   // 4. Persisti
   lSuccess := TCustomers():Update( nId, oVal:DataFields(), @cError )
   ...
RETURN nil

Action Delete

Stesso pattern - il token viene dal form di conferma di cancellazione:

METHOD Delete() CLASS Customer
   LOCAL cId := UGetResource()
   ...

Anatomia del token

   payload (base64)        .   firma (HMAC-SHA256)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”.β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  MTAwfDE3MzQ4OTAxMjM=    .   aBcD3f9eGgHhIi...
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        └── base64Decode -> "100|1734890123"
                           β”‚      β”‚
                           β”‚      └── timestamp unix
                           └── ID originale
  • Il payload porta l'ID originale separato da | dal timestamp.
  • La firma Γ¨ calcolata come HMAC-SHA256( payload, app_key ).
  • Se qualcuno altera anche solo un byte del payload, la firma non corrisponde piΓΉ e HIX_TokenValid ritorna .F..
  • Il timestamp permette di implementare la scadenza (non usato da UGetResource, che valida con nLapsus = 0).

Il segreto - app_key

UResourceToHtml e UGetResource condividono lo stesso app_key del CSRF. Configuralo una volta:

HIX_ConfigAppSet( "app_key", "my_secret_app_key" )

⚠️ Cambiare app_key invalida tutti i token firmati: CSRF, Resource IDs e qualsiasi altro HIX_TokenMake legato allo stesso segreto. I form aperti in tab attive daranno errore finché non si aggiorna.


Pattern completo di edit/update con Fenix

GET /customer/42/edit - renderizza il form

METHOD Edit() CLASS Customer
   LOCAL oVal := UValidateParams( { "id" => { "required|number|min:0", "Id" } } )
   LOCAL hRow

   IF ! oVal:Make()
      RETURN URedirect( URoute( "customer.search" ) )
   ENDIF

   TCustomers():GetRecno( oVal:Get( "id" ), @hRow, NIL, .T. )

RETURN UView( "masters/customer/edit.html", .T., hRow )

Template - edit.html

@args lEdit, hRow

<form method="POST" action="{{ URoute('customer.update', hRow['recno']) }}">
  {{ UCsrfToHtml() }}
  {{ UResourceToHtml( hRow['recno'] ) }}

  <label>Nome <input name="first" value="{{ hRow['first'] }}"></label>
  <label>CittΓ   <input name="city"  value="{{ hRow['city']  }}"></label>
  ...
  <button>Salva</button>
</form>

POST /customer/42/edit - riceve il form

METHOD Update() CLASS Customer
   LOCAL cId := UGetResource()           // 42 firmato e validato
   ...

Confronto con CSRF

Token CSRF Token Resource ID
Cosa firma niente - solo timestamp random L'ID della risorsa
Nome campo _csrf _resource_id
Scopo "Questo form Γ¨ stato lanciato dalla nostra pagina, non da un attaccante" "Questo ID Γ¨ quello che ti ho dato io, non uno manipolato"
Helper di render UCsrfToHtml() UResourceToHtml( cId )
Helper di lettura (automatico via middleware) UGetResource()
Segreto condiviso βœ… app_key βœ… app_key

Di solito vanno insieme in ogni form (CSRF + Resource ID).


Best practice

  1. CSRF + Resource ID in ogni form di edit/delete. Sono complementari.
  2. Non sostituisce i controlli di permesso. Un token Resource ID valido dice solo "questo Γ¨ l'ID che ti ho dato"; devi comunque controllare che l'utente possa modificare quella risorsa.
  3. Non mettere dati sensibili nell'ID. Il payload è solo base64, non cifrato - chiunque può leggere l'ID originale. È solo tamper-proof, non confidenziale.
  4. Cambia app_key in produzione. Se mantieni il default pubblicato nel repo, chiunque puΓ² generare token validi.