π Resource IDs¶
Quale problema risolvono?¶
Un tipico form di modifica incorpora l'ID della risorsa come campo nascosto:
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:
- Ricontrollare i permessi sul
99lato server (l'approccio giusto, ma costoso e facile da dimenticare). - Firmare l'ID così che il client non possa alterarlo - questa è la soluzione che
UResourceToHtmlfornisce.
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¶
<form method="POST" action="/customer/edit">
{{ UCsrfToHtml() }}
{{ UResourceToHtml( nRecno ) }}
<input name="first" value="{{ hRow['first'] }}">
...
<button>Salva</button>
</form>
UResourceToHtml( "100" ) genera qualcosa come:
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:
UPost( "_resource_id" )- prima dal body del POSTUGet( "_resource_id" )- fallback nella query string- 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:
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_TokenValidritorna.F.. - Il timestamp permette di implementare la scadenza (non usato da
UGetResource, che valida connLapsus = 0).
Il segreto - app_key¶
UResourceToHtml e UGetResource condividono lo stesso app_key del CSRF. Configuralo una volta:
β οΈ Cambiare
app_keyinvalida tutti i token firmati: CSRF, Resource IDs e qualsiasi altroHIX_TokenMakelegato 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¶
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¶
- CSRF + Resource ID in ogni form di edit/delete. Sono complementari.
- 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.
- Non mettere dati sensibili nell'ID. Il payload Γ¨ solo base64, non cifrato - chiunque puΓ² leggere l'ID originale. Γ solo tamper-proof, non confidenziale.
- Cambia
app_keyin produzione. Se mantieni il default pubblicato nel repo, chiunque puΓ² generare token validi.