Harden for public launch: rate limiting, CORS, error display, Gitea webhook sig

- User::login() locks an account for 15 min after 5 failed attempts
- CORS now restricted to an explicit origin whitelist instead of *
- display_errors disabled in production (errors still logged server-side)
- webhook.php now checks Gitea's actual signature header (X-Gitea-Signature,
  raw hex) instead of GitHub's format, which never matched on this Gitea instance

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 17:53:28 +02:00
parent 4c61b7f276
commit 42a388cbe1
3 changed files with 66 additions and 23 deletions
+46 -17
View File
@@ -4,6 +4,8 @@
class User {
private $conn;
private $table_name = "users";
private $max_attempts = 5;
private $lockout_minutes = 15;
public function __construct($db) {
$this->conn = $db;
@@ -44,28 +46,55 @@ class User {
// --- 2. CONNEXION ---
public function login($email, $password) {
$query = "SELECT id, username, email, password, pairing_code, partner_id FROM " . $this->table_name . " WHERE email = :email LIMIT 0,1";
$query = "SELECT id, username, email, password, pairing_code, partner_id, failed_attempts, locked_until FROM " . $this->table_name . " WHERE email = :email LIMIT 0,1";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(":email", $email);
$stmt->execute();
if ($stmt->rowCount() > 0) {
$row = $stmt->fetch();
// Vérification du mot de passe
if (password_verify($password, $row['password'])) {
return [
"success" => true,
"user" => [
"id" => $row['id'],
"username" => $row['username'],
"email" => $row['email'],
"pairing_code" => $row['pairing_code'],
"partner_id" => $row['partner_id']
]
];
}
if ($stmt->rowCount() === 0) {
return ["success" => false, "message" => "Identifiants incorrects."];
}
$row = $stmt->fetch();
// Compte temporairement verrouillé après trop d'échecs
if ($row['locked_until'] && strtotime($row['locked_until']) > time()) {
return ["success" => false, "message" => "Trop de tentatives échouées. Réessaie dans quelques minutes."];
}
// Vérification du mot de passe
if (password_verify($password, $row['password'])) {
$reset = $this->conn->prepare("UPDATE " . $this->table_name . " SET failed_attempts = 0, locked_until = NULL WHERE id = :id");
$reset->bindParam(":id", $row['id']);
$reset->execute();
return [
"success" => true,
"user" => [
"id" => $row['id'],
"username" => $row['username'],
"email" => $row['email'],
"pairing_code" => $row['pairing_code'],
"partner_id" => $row['partner_id']
]
];
}
// Échec : on incrémente le compteur et on verrouille si le seuil est atteint
$attempts = $row['failed_attempts'] + 1;
if ($attempts >= $this->max_attempts) {
$lock = $this->conn->prepare("UPDATE " . $this->table_name . " SET failed_attempts = :attempts, locked_until = DATE_ADD(NOW(), INTERVAL :minutes MINUTE) WHERE id = :id");
$lock->bindParam(":attempts", $attempts, PDO::PARAM_INT);
$lock->bindParam(":minutes", $this->lockout_minutes, PDO::PARAM_INT);
$lock->bindParam(":id", $row['id']);
$lock->execute();
} else {
$incr = $this->conn->prepare("UPDATE " . $this->table_name . " SET failed_attempts = :attempts WHERE id = :id");
$incr->bindParam(":attempts", $attempts, PDO::PARAM_INT);
$incr->bindParam(":id", $row['id']);
$incr->execute();
}
return ["success" => false, "message" => "Identifiants incorrects."];
}