Hello les petits Biscuits !
Bienvenue sur la 51Úme édition de Ruby Biscuit.
Vous ĂȘtes maintenant 598 abonnĂ©s đ„ł
Cette édition de Ruby Biscuit est sponsorisée par AppSignal.
Le monitoring Ruby, sans multiplier les outils
Vous utilisez plusieurs outils pour surveiller vos applications Ruby ? Entre les erreurs, les requĂȘtes N+1, Sidekiq, les logs et les mĂ©triques systĂšme, difficile dâavoir une vue dâensemble.
AppSignal rassemble tout ça au mĂȘme endroit, dans une seule gem Ruby : suivi des erreurs, dĂ©tection des requĂȘtes N+1, monitoring de Sidekiq, logs et mĂ©triques systĂšme.
Lâobjectif : identifier plus facilement les problĂšmes de vos applications sans multiplier les outils de monitoring.
Des utilisateurs illimités, une seule facture.
đ Essayez AppSignal gratuitement pendant 30 jours, sans carte bancaire.
đ DĂ©couvrir AppSignal

Bonne lecture.
Vous lisez Ruby Biscuit, la newsletter Rails de Capsens. Sâabonner gratuitement
Un compte authentifiĂ© qui tĂ©lĂ©charge des documents ne viole aucune rĂšgle : le mot de passe est bon, les droits sont corrects, chaque requĂȘte est valide. Seul lâensemble de ses requĂȘtes peut ĂȘtre anormal, et une application ne le voit pas requĂȘte par requĂȘte. Cet article construit, avec Rails et Redis, une dĂ©tection qui compare chaque compte Ă son propre historique : des compteurs par utilisateur, une baseline robuste, un dĂ©tecteur dâĂ©numĂ©ration dâidentifiants, un score combinĂ©, puis la marche Ă suivre quand un compte est signalĂ©.
OĂč en est lâenquĂȘte ? Un virement de 15 000 ⏠nâest jamais parti : son job a attendu derriĂšre des milliers de jobs de filigrane, tous dĂ©clenchĂ©s par les tĂ©lĂ©chargements dâun seul compte, en rafale le mardi soir. Reste Ă savoir si ce compte est utilisĂ© par son propriĂ©taire, et câest la question de cet article.
Cette sĂ©rie suit un mĂȘme incident, en fil rouge, au fil des techniques abordĂ©es. Les encadrĂ©s « âȘ Ce que vous auriez vu » montrent les lignes de log ou les Ă©crans que le bon outillage aurait produits cette nuit-lĂ , et qui nâont jamais existĂ© faute de prĂ©paration. Vous pouvez les sauter : le reste se lit comme un tutoriel autonome.
Pourquoi un seuil ne suffit pas
DĂ©tecter un comportement anormal revient Ă mesurer un Ă©cart, et la premiĂšre question est : par rapport Ă quoi ? Lâapproche la plus courante rĂ©pond par un plafond fixe, par exemple un nombre maximal de tĂ©lĂ©chargements par jour. Prenons un compte qui tĂ©lĂ©charge beaucoup de documents : un client zĂ©lĂ©, un cabinet dâaudit mandatĂ© ou un intrus produisent la mĂȘme courbe de volume, et ce plafond doit pourtant les dĂ©partager.
Ce plafond se trompe aux deux extrĂ©mitĂ©s. Pour attraper un compte qui tĂ©lĂ©charge quelques dizaines de documents par jour, il faudrait le fixer Ă moins de trente tĂ©lĂ©chargements par jour : il sonnerait alors pour tout client qui rĂ©cupĂšre ses relevĂ©s annuels en une soirĂ©e, et lâĂ©quipe finirait par ne plus lire ces alertes. FixĂ© Ă cent tĂ©lĂ©chargements quotidiens, il laisse passer ce mĂȘme compte jour aprĂšs jour. Le plafond garde pourtant sa place Ă un autre niveau : fixĂ© trĂšs haut, il empĂȘche un seul compte de vider la base le temps quâune alerte soit lue. Il ne dĂ©tecte rien de subtil, et câest ce que cherche cet article.
La question « combien est-ce trop ? » nâa pas de rĂ©ponse unique, car elle dĂ©pend de qui tĂ©lĂ©charge. On peut en poser une autre : cet utilisateur ressemble-t-il Ă lui-mĂȘme ?
Une baseline est la description statistique du comportement habituel dâun utilisateur (combien de documents il consulte par jour, depuis oĂč, Ă quelle heure), Ă laquelle on compare son comportement du moment.
Construire la baseline dâun utilisateur
Quatre dimensions suffisent pour commencer : le volume dâaccĂšs par jour, la diversitĂ© des ressources touchĂ©es, la plage horaire et la stabilitĂ© des points dâaccĂšs (adresses, navigateur). Tout repose sur des compteurs quotidiens par utilisateur, alimentĂ©s comme Ă lâarticle 1 par lâĂ©vĂ©nement que Rails publie Ă chaque requĂȘte. Le contrĂŽleur y ajoute lâutilisateur courant, lâadresse et le navigateur. Ces compteurs vivent dans Redis plutĂŽt que dans lâoutil de logs de lâarticle 3, que le job de dĂ©tection nâa pas Ă interroger chaque nuit pour chaque compte.
# app/controllers/application_controller.rb
def append_info_to_payload(payload)
super
payload[:user_id] = current_user&.id
payload[:remote_ip] = request.remote_ip
payload[:user_agent] = request.user_agent
end
# config/initializers/behavior_metrics.rb
WINDOW_TTL = 35.days.to_i
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |event|
payload = event.payload
user_id = payload[:user_id]
doc_id = payload[:params]["id"].to_s.first(64)
ip = payload[:remote_ip].to_s.first(45)
agent = payload[:user_agent].to_s.first(200)
next unless user_id && doc_id.present? && payload[:controller] == "DocumentsController"
day = Date.current.iso8601
keys = %w[count ids ips uas].map { |kind| "docs:#{kind}:#{user_id}:#{day}" }
REDIS.pipelined do |pipe|
pipe.incr(keys[0])
pipe.sadd(keys[1], doc_id)
pipe.sadd(keys[2], ip)
pipe.sadd(keys[3], agent)
keys.each { |key| pipe.expire(key, WINDOW_TTL) }
end
rescue StandardError => e
Rails.logger.error("[behavior] #{e.class}: #{e.message}")
endLes clĂ©s portent la date et expirent seules, ce qui donne une fenĂȘtre glissante sans nettoyage Ă Ă©crire. Le rescue final reste indispensable : comme vu Ă lâarticle 1, un Redis indisponible ferait sinon Ă©chouer la requĂȘte. La plage horaire suit le mĂȘme principe, avec un compteur par heure.
La mĂ©diane remplace la moyenne, car quelques jours aberrants dĂ©placent la moyenne, et un attaquant patient sâen sert pour que la suite paraisse normale. Pour la dispersion, mĂȘme logique avec lâĂ©cart absolu mĂ©dian (la mĂ©diane des Ă©carts Ă la mĂ©diane), plus robuste que lâĂ©cart-type sur des comportements humains irrĂ©guliers. LâĂ©cart du jour se mesure en multiples de cette dispersion. Une quarantaine de documents donne ainsi un Ă©cart quasi nul pour un cabinet dâaudit qui en tĂ©lĂ©charge autant chaque jour ouvrĂ©, et un Ă©cart trĂšs Ă©levĂ© pour un investisseur qui les tĂ©lĂ©charge un mardi.
# app/services/baseline.rb
class Baseline
WINDOW_DAYS = 28
MAD_FLOOR = 1.0 # valeur indicative
def initialize(user_id, today: Date.current)
@user_id = user_id
@today = today
end
# Ăcart du jour, en multiples de la dispersion habituelle du compte.
def deviation
history = (1..WINDOW_DAYS).map { |i| count_for(@today - i) }
center = median(history)
spread = [median(history.map { |c| (c - center).abs }), MAD_FLOOR].max
(count_for(@today) - center) / spread
end
private
def count_for(day)
REDIS.get("docs:count:#{@user_id}:#{day.iso8601}").to_i
end
def median(values)
sorted = values.sort
mid = sorted.size / 2
sorted.size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0
end
endCe calcul nâa rien Ă faire dans la requĂȘte : un job nocturne lâexĂ©cute sur la journĂ©e Ă©coulĂ©e pour les comptes actifs, sur une file dĂ©diĂ©e Ă lâanalyse et jamais sur celle des paiements. Lâincident est nĂ© dâun dispositif de traçabilitĂ© placĂ© au mauvais endroit, et la dĂ©tection ne doit pas rĂ©pĂ©ter lâerreur du filigrane.
Quelques remarques
Un compte qui se connecte rarement a une médiane et une dispersion nulles. Sans le plancher
MAD_FLOOR, son premier tĂ©lĂ©chargement donnerait un Ă©cart infini.La baseline se laisse empoisonner. DĂšs que les jours anormaux dĂ©passent la moitiĂ© de la fenĂȘtre, la mĂ©diane les adopte pour normaux ; on exclut donc de la baseline les jours dĂ©jĂ signalĂ©s.
Un compte neuf nâa pas dâhistorique, et tout lui paraĂźt inconnu. On neutralise pendant ses premiĂšres semaines les signaux qui comparent Ă lâhistorique, mais on garde la densitĂ© de la suite, qui nâen dĂ©pend pas : sans cela, un compte créé exprĂšs pour Ă©numĂ©rer passerait sous le radar.
Ces compteurs restent des donnĂ©es personnelles, une adresse et un navigateur rattachĂ©s Ă un compte formant un profil. On garde les compteurs le temps de la fenĂȘtre, les adresses et navigateurs connus plus longtemps (400 jours ici), la finalitĂ© comme la durĂ©e sâinscrivent au registre des traitements, et on ne stocke jamais de contenu de document. Enfin,
request.remote_ipnâest fiable que si les proxys de confiance sont bien dĂ©clarĂ©s, faute de quoi on compare des adresses de proxy.Gardez ces compteurs dans un Redis distinct de celui de Sidekiq : on veut pouvoir leur appliquer une politique dâĂ©viction, ce quâon Ă©vite pour la file de jobs.
Détecter une énumération
Une IDOR (Insecure Direct Object Reference) survient quand une application sert une ressource Ă partir dâun identifiant fourni par le client, sans vĂ©rifier que cette ressource appartient Ă celui qui la demande. Changez le
42dâune URL en43: si le document dâun autre client sâaffiche, lâapplication est vulnĂ©rable.
Celui qui exploite ce dĂ©faut doit essayer des identifiants, et la forme de ses essais le trahit. Un investisseur relit ses propres documents, toujours les mĂȘmes et dans le dĂ©sordre. LâĂ©numĂ©rateur, lui, parcourt une suite : identifiants consĂ©cutifs ou presque, trous qui produisent des 404, ressources jamais consultĂ©es auparavant. Deux ratios traduisent cela : la part dâidentifiants nouveaux, et la densitĂ© de la suite (identifiants distincts rapportĂ©s Ă lâĂ©tendue quâils couvrent).
# app/services/enumeration_check.rb
class EnumerationCheck
WINDOW_DAYS = 28
MIN_IDS = 20
def initialize(user_id, day: Date.current)
@user_id = user_id
@day = day
end
# Part des identifiants du jour absents de la fenĂȘtre prĂ©cĂ©dente.
def new_ratio
return 0.0 if today_ids.empty?
seen = REDIS.sunion(*(1..WINDOW_DAYS).map { |i| ids_key(@day - i) })
(today_ids - seen).size.fdiv(today_ids.size)
end
# Proche de 1 : une suite consécutive. Proche de 0 : des identifiants dispersés.
def density
ids = today_ids.map(&:to_i).sort
return 0.0 if ids.size < MIN_IDS
ids.size.fdiv(ids.last - ids.first + 1)
end
private
def today_ids
@today_ids ||= REDIS.smembers(ids_key(@day))
end
def ids_key(day)
"docs:ids:#{@user_id}:#{day.iso8601}"
end
endCes ratios mesurent la forme des accĂšs plus que leur volume. La densitĂ© nâa de sens quâĂ partir dâun certain nombre dâidentifiants : trois documents gĂ©nĂ©rĂ©s ensemble (un contrat, une attestation, un reçu) se suivent aussi, dâoĂč un minimum fixĂ© dans le code. Un Ă©numĂ©rateur qui reste sous ce minimum Ă©chappe Ă ce signal, mais pas aux autres sur un compte ancien.
Tout ce qui prĂ©cĂšde est une deuxiĂšme ligne de dĂ©fense. La premiĂšre est lâautorisation. Document.find(params[:id]) sert nâimporte quel document Ă qui en connaĂźt lâidentifiant, alors que current_user.documents.find(params[:id]) lĂšve une ActiveRecord::RecordNotFound (une 404) pour tout document qui nâest pas le sien. LâIDOR disparaĂźt avec cette ligne, et aucune dĂ©tection ne la remplace. Quand les rĂšgles dĂ©passent la simple appartenance, on les centralise dans des politiques dâautorisation, dont Pundit et Action Policy sont deux implĂ©mentations courantes.
Note : des identifiants sĂ©quentiels facilitent lâĂ©numĂ©ration, des identifiants opaques (UUID, valeurs alĂ©atoires) la rendent coĂ»teuse sans la rendre impossible. Ils ne dispensent pas du contrĂŽle dâaccĂšs : un identifiant difficile Ă deviner peut se lire ailleurs, dans un log par exemple (article 2). Avec des UUID, la densitĂ© de la suite nâa plus de sens, et seule la part de ressources nouvelles reste utilisable.
Combiner les signaux
Un signal seul produit souvent de faux positifs : un dĂ©placement change dâadresse, un nouveau tĂ©lĂ©phone change de navigateur, un VPN fait les deux. Trois signaux se calculent Ă peu de frais avec les compteurs : une adresse jamais vue sur le compte depuis un an, une empreinte de client inhabituelle (un navigateur jamais vu) et des sessions concurrentes (plusieurs adresses distinctes le mĂȘme jour). Un quatriĂšme, lâincohĂ©rence gĂ©ographique dans un intervalle trop court, demande une base de gĂ©olocalisation dâadresses et reste hors du code ci-dessous. CombinĂ©s avec lâĂ©cart de volume, retenu au-delĂ de quelques multiples de la dispersion, et les ratios dâĂ©numĂ©ration, ils donnent Ă chaque compte un score.
# app/services/risk_score.rb
class RiskScore
# Poids et seuils indicatifs : les vĂŽtres se rĂšglent par observation.
WEIGHTS = {
unknown_ip: 2,
unknown_user_agent: 1,
volume_deviation: 2,
new_resources: 1,
dense_ids: 4,
parallel_ips: 2
}.freeze
DIGEST_AT = 4
ALERT_AT = 8
# signals : { unknown_ip: true, volume_deviation: false, ... }
def initialize(signals)
@signals = signals
end
def value
WEIGHTS.sum { |name, weight| @signals[name] ? weight : 0 }
end
def level
return :alert if value >= ALERT_AT
return :digest if value >= DIGEST_AT
:none
end
end
# app/jobs/behavior_scan_job.rb
class BehaviorScanJob
include Sidekiq::Job
sidekiq_options queue: :analytics, retry: 1
# Seuils indicatifs, comme les poids de RiskScore.
WINDOW_DAYS = 28
KNOWN_TTL = 400.days.to_i
VOLUME_AT = 5.0
NEW_RATIO_AT = 0.8
DENSITY_AT = 0.5
PARALLEL_IPS_AT = 3
def perform(user_id)
day = Date.yesterday # le job tourne la nuit : on analyse la journée écoulée
enumeration = EnumerationCheck.new(user_id, day: day)
# Signaux qui ne dépendent d'aucun historique : valables pour un compte neuf.
signals = {
dense_ids: enumeration.density >= DENSITY_AT,
parallel_ips: REDIS.scard("docs:ips:#{user_id}:#{day.iso8601}") >= PARALLEL_IPS_AT
}
# Signaux relatifs Ă l'historique : comptes de plus de 28 jours seulement.
if User.find(user_id).created_at <= WINDOW_DAYS.days.ago
signals.merge!(
unknown_ip: unseen?("ips", user_id, day),
unknown_user_agent: unseen?("uas", user_id, day),
volume_deviation: Baseline.new(user_id, today: day).deviation >= VOLUME_AT,
new_resources: enumeration.new_ratio >= NEW_RATIO_AT
)
end
score = RiskScore.new(signals)
if score.level == :none
remember(user_id, day)
return
end
active = signals.select { |_, on| on }.keys.join(",")
Rails.logger.warn("[security] behavior #{score.level} user_id=#{user_id} score=#{score.value} signals=#{active}")
end
private
# Vrai si une valeur du jour (adresse, navigateur) n'a jamais été vue sur le compte.
def unseen?(kind, user_id, day)
today = REDIS.smembers("docs:#{kind}:#{user_id}:#{day.iso8601}")
(today - REDIS.smembers("docs:known_#{kind}:#{user_id}")).any?
end
# Mémoire plus longue que les compteurs : seules les journées sans alerte l'enrichissent.
def remember(user_id, day)
%w[ips uas].each do |kind|
today = REDIS.smembers("docs:#{kind}:#{user_id}:#{day.iso8601}")
next if today.empty?
REDIS.sadd("docs:known_#{kind}:#{user_id}", today)
REDIS.expire("docs:known_#{kind}:#{user_id}", KNOWN_TTL)
end
end
endCe job assemble les signaux de chaque compte actif chaque nuit et Ă©crit une ligne de log quand le score atteint un niveau. Le client zĂ©lĂ© de la premiĂšre section, mĂȘme sâil ne vient quâune fois par an, ne cumule que des signaux faibles, le volume et des ressources nouvelles : son score reste sous le premier seuil, parce que ses adresses et navigateurs restent en mĂ©moire plus longtemps que les compteurs.
Note : les poids et les seuils de ces extraits sont indicatifs, ce ne sont pas nos valeurs. Les vÎtres se rÚglent sur vos propres données, et gagnent à rester confidentielles.
La rafale du mardi soir, avec ses connexions simultanĂ©es depuis des adresses inconnues sur un compte habituellement connectĂ© depuis un seul appareil, aurait fait monter plusieurs signaux dâun coup.
âȘ Ce que vous auriez vu. Dans la nuit qui suit la premiĂšre Ă©numĂ©ration, une alerte :
[security] behavior alert user_id=4217 score=10 signals=dense_ids,unknown_ip,unknown_user_agent,volume_deviation,new_resources. Elle nâa jamais existĂ©. Elle serait partie dix jours avant la rafale, avant le premier job de filigrane en retard, et le virement nâaurait jamais Ă©tĂ© inquiĂ©tĂ©.
Alerter plutĂŽt que bloquer
Le score alimente deux rĂ©gimes. Un signal fort et rare part immĂ©diatement vers quelquâun : câest le niveau :alert, celui de notre compte. Un signal faible et persistant sâaccumule dans un rapport quotidien quâune personne lit vraiment : câest le niveau :digest, celui dâune adresse nouvelle accompagnĂ©e dâun volume Ă©levĂ©, ou dâun cabinet dâaudit qui ouvre un dossier dont les documents se suivent. Dans les deux cas on prĂ©vient un humain plutĂŽt que de bloquer, car le blocage automatique se paie en faux positifs sur des utilisateurs lĂ©gitimes, et câest le sujet dâun prochain article. Le job tourne la nuit : entre une rafale et lâalerte, il peut sâĂ©couler jusquâĂ une journĂ©e, et rien dans ce dispositif ne lâarrĂȘte. Câest le rĂŽle du plafond Ă©voquĂ© en dĂ©but dâarticle, et les deux se complĂštent.
Une dĂ©tection a besoin dâun destinataire et dâune procĂ©dure : qui reçoit, qui dĂ©cide de contacter le titulaire, qui peut geler le compte, en combien de temps. Sans ces rĂ©ponses Ă©crites Ă lâavance, lâalerte reste sans suite. Le job Ă©crit sa ligne dans les logs centralisĂ©s de lâarticle 3, oĂč lâalerte se branche comme les autres.
Que faire dâun compte compromis
Une fois lâalerte reçue, les gestes immĂ©diats sont dâinvalider toutes les sessions du compte, de forcer la rĂ©initialisation du mot de passe, dâexiger un second facteur Ă la reconnexion et de geler les opĂ©rations sensibles (virement, changement dâIBAN ou dâadresse e-mail). Avec Devise, la session contient un sel dĂ©rivĂ© du mot de passe : le seul moyen natif de la couper est de le changer, ce qui suppose que le titulaire rĂ©ponde Ă lâe-mail. Pour couper sans attendre, on ajoute un jeton de session au sel.
# app/models/user.rb (colonnes session_token et frozen_at ajoutées par migration)
class User < ApplicationRecord
devise :database_authenticatable, :recoverable, :rememberable
# Changer ce jeton invalide tous les cookies de session du compte.
def authenticatable_salt
"#{super}#{session_token}"
end
def lock_down!
update!(session_token: SecureRandom.hex(16), frozen_at: Time.current)
forget_me!
send_reset_password_instructions
end
endLes contrĂŽleurs sensibles (virement, changement dâIBAN) refusent lâaction tant que frozen_at est renseignĂ©.
Note : avec lâauthentification native de Rails 8, les sessions sont des lignes de la table sessions, et user.sessions.destroy_all suffit.
Note : avant dâenvoyer le lien de rĂ©initialisation, vĂ©rifiez que lâadresse e-mail et le tĂ©lĂ©phone du compte nâont pas Ă©tĂ© modifiĂ©s par lâattaquant, faute de quoi le lien part dans ses mains.
PrĂ©parez aussi la liste des documents consultĂ©s pendant la compromission : elle dĂ©termine qui informer et ce quâon dĂ©clare Ă la CNIL.
Sur le plan rĂ©glementaire, des piĂšces dâidentitĂ© et des justificatifs qui sortent forment une violation de donnĂ©es personnelles. Le responsable de traitement la notifie Ă lâautoritĂ© de contrĂŽle (en France, la CNIL) dans les meilleurs dĂ©lais et, si possible, 72 heures au plus tard aprĂšs en avoir pris connaissance, sauf sâil est peu probable quâelle entraĂźne un risque pour les personnes (RGPD, article 33). Il informe aussi les personnes concernĂ©es lorsque le risque est Ă©levĂ© (article 34), ce quâon peut raisonnablement envisager ici vu lâusurpation dâidentitĂ© et la fraude documentaire. Un sous-traitant alerte, lui, le responsable de traitement dans les meilleurs dĂ©lais (article 33, paragraphe 2).
CĂŽtĂ© LCB-FT, ces piĂšces ont Ă©tĂ© recueillies pour identifier et vĂ©rifier lâidentitĂ© des clients (article L. 561-5 du code monĂ©taire et financier) : entre de mauvaises mains, elles permettent dâouvrir des comptes ailleurs. CĂŽtĂ© DORA, lâincident peut relever du rĂ©gime de dĂ©claration vu Ă lâarticle 1 sâil est classĂ© majeur, et le service externe de gĂ©nĂ©ration de PDF est un prestataire de services TIC de lâentitĂ© financiĂšre cliente, dont la dĂ©faillance a amplifiĂ© lâincident : câest le terrain du chapitre V du rĂšglement, consacrĂ© au risque liĂ© aux tiers.
Note : la notification peut se faire par phases quand toutes les informations manquent (article 33, paragraphe 4).
Chacune de ces obligations suppose une chronologie horodatée et la liste des personnes et des documents concernés, que les logs des articles 2 et 3 permettent de produire.
Retour sur lâenquĂȘte
Dans les logs centralisĂ©s, les requĂȘtes de notre compte sur /documents/:id/download suivent une suite : 7301, 7302, 7304, avec des trous qui produisent des 404. Les trois adresses de la rafale, gĂ©olocalisĂ©es, tombent dans trois pays distincts, aucun nâĂ©tant celui dâoĂč le titulaire se connectait jusque-lĂ , et la premiĂšre connexion depuis une adresse inconnue date de la veille du premier tĂ©lĂ©chargement. Le compte nâavait pas activĂ© de second facteur.
Il avait Ă©tĂ© compromis, trĂšs probablement par hameçonnage ciblĂ©, ce quâaucun log de lâapplication ne peut confirmer. Une IDOR sur /documents/:id/download a permis dâĂ©numĂ©rer les documents des autres clients, lentement dâabord, puis en rafale : environ 2 900 documents emportĂ©s, dont des piĂšces dâidentitĂ© et des justificatifs de connaissance client (KYC).
Chaque document emportĂ© dĂ©clenchait un job de filigrane, et câest ce stock de jobs qui a retardĂ© le virement : la panne de paiement et lâexfiltration relĂšvent dâun seul Ă©vĂ©nement. Le passĂ© est reconstituĂ©, Ă lâhameçonnage prĂšs, et la prochaine fois reste possible.
En deux mots
Comparer chaque compte Ă son propre historique permet de repĂ©rer une phase lente quâun plafond global ne voit pas : des compteurs par utilisateur, une mĂ©diane, la forme de la suite dâidentifiants demandĂ©s, un score qui combine ces signaux et une procĂ©dure prĂȘte pour le compte signalĂ©. Cela demande de conserver des donnĂ©es de comportement, de trier des faux positifs chaque matin et dâĂ©crire cette procĂ©dure avant dâen avoir besoin. Et cela ne remplace pas le contrĂŽle dâaccĂšs, qui aurait rendu cette attaque impossible.
Une dĂ©tection ne bloque rien : elle prĂ©vient quelquâun, aprĂšs coup, dâoĂč lâintĂ©rĂȘt dâun plafond trĂšs haut en garde-fou. Le prochain article de la sĂ©rie regarde ce quâon peut refuser automatiquement, et ce que cela coĂ»te aux utilisateurs lĂ©gitimes.
â InĂšs, Responsable de la SĂ©curitĂ© des SystĂšmes dâInformation chez Capsens
Pour aller plus loin
Notification des violations et information des personnes : RĂšglement (UE) 2016/679, articles 33 et 34, EUR-Lex
Gestion et déclaration des incidents liés aux TIC : RÚglement (UE) 2022/2554 (DORA), article 19 et chapitre V, EUR-Lex
Délais et contenu des notifications DORA : RÚglement délégué (UE) 2025/301, EUR-Lex
Classification des incidents DORA : RÚglement délégué (UE) 2024/1772, EUR-Lex
Identification et vĂ©rification de lâidentitĂ© des clients : Article L. 561-5 du code monĂ©taire et financier, LĂ©gifrance
ContrĂŽle dâaccĂšs au niveau de lâobjet : Insecure Direct Object Reference Prevention Cheat Sheet, OWASP
Enrichir le payload dâune requĂȘte : ActionController::Instrumentation, API Rails
Abonnement aux Ă©vĂ©nements de requĂȘte : Active Support Instrumentation, guides Rails
Sel de session et invalidation : Devise, lib/devise/models/authenticatable.rb, dépÎt heartcombo/devise
Ajout Ă un ensemble : SADD, documentation Redis
Union dâensembles : SUNION, documentation Redis
Compteurs : INCR, documentation Redis
Médiane et écart absolu médian : Detecting outliers: Do not use standard deviation around the mean, use absolute deviation around the median, Leys et al., Journal of Experimental Social Psychology
Si cet article vous a été utile, partagez-le à un(e) collÚgue.


