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:
@@ -1,12 +1,24 @@
|
|||||||
<?php
|
<?php
|
||||||
// index.php
|
// index.php
|
||||||
|
|
||||||
ini_set('display_errors', 1);
|
// En prod on ne balance plus les erreurs PHP au visiteur (fuite d'infos internes) ;
|
||||||
ini_set('display_startup_errors', 1);
|
// elles restent loguées côté serveur via error_log.
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
ini_set('display_startup_errors', 0);
|
||||||
|
ini_set('log_errors', 1);
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
// --- HEADERS CORS (Indispensables pour ton site et ton appli mobile) ---
|
// --- HEADERS CORS ---
|
||||||
header("Access-Control-Allow-Origin: *");
|
// Les apps mobiles natives ne sont pas concernées par CORS (c'est une restriction de navigateur),
|
||||||
|
// donc restreindre ici n'affecte que les navigateurs web.
|
||||||
|
$allowed_origins = [
|
||||||
|
'https://watchgether.whykioh.fr',
|
||||||
|
'null', // ouverture du HTML en double-clic (file://) pendant le dev local
|
||||||
|
];
|
||||||
|
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||||
|
if (in_array($origin, $allowed_origins, true)) {
|
||||||
|
header("Access-Control-Allow-Origin: $origin");
|
||||||
|
}
|
||||||
header("Content-Type: application/json; charset=UTF-8");
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
|
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
|
||||||
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
|
header("Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
|
||||||
|
|||||||
+31
-2
@@ -4,6 +4,8 @@
|
|||||||
class User {
|
class User {
|
||||||
private $conn;
|
private $conn;
|
||||||
private $table_name = "users";
|
private $table_name = "users";
|
||||||
|
private $max_attempts = 5;
|
||||||
|
private $lockout_minutes = 15;
|
||||||
|
|
||||||
public function __construct($db) {
|
public function __construct($db) {
|
||||||
$this->conn = $db;
|
$this->conn = $db;
|
||||||
@@ -44,16 +46,28 @@ class User {
|
|||||||
|
|
||||||
// --- 2. CONNEXION ---
|
// --- 2. CONNEXION ---
|
||||||
public function login($email, $password) {
|
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 = $this->conn->prepare($query);
|
||||||
$stmt->bindParam(":email", $email);
|
$stmt->bindParam(":email", $email);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
|
|
||||||
if ($stmt->rowCount() > 0) {
|
if ($stmt->rowCount() === 0) {
|
||||||
|
return ["success" => false, "message" => "Identifiants incorrects."];
|
||||||
|
}
|
||||||
|
|
||||||
$row = $stmt->fetch();
|
$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
|
// Vérification du mot de passe
|
||||||
if (password_verify($password, $row['password'])) {
|
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 [
|
return [
|
||||||
"success" => true,
|
"success" => true,
|
||||||
"user" => [
|
"user" => [
|
||||||
@@ -65,7 +79,22 @@ class User {
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// É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."];
|
return ["success" => false, "message" => "Identifiants incorrects."];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -14,8 +14,10 @@ $database = new Database();
|
|||||||
$secret = $database->webhook_secret;
|
$secret = $database->webhook_secret;
|
||||||
|
|
||||||
$payload = file_get_contents('php://input');
|
$payload = file_get_contents('php://input');
|
||||||
$signatureHeader = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
|
// Gitea (auto-hébergé) envoie un hex brut dans X-Gitea-Signature, sans préfixe "sha256="
|
||||||
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);
|
// (contrairement à GitHub qui utilise X-Hub-Signature-256 avec le préfixe)
|
||||||
|
$signatureHeader = $_SERVER['HTTP_X_GITEA_SIGNATURE'] ?? '';
|
||||||
|
$expectedSignature = hash_hmac('sha256', $payload, $secret);
|
||||||
|
|
||||||
// Comparaison en temps constant pour éviter les attaques par timing
|
// Comparaison en temps constant pour éviter les attaques par timing
|
||||||
if (!$signatureHeader || !hash_equals($expectedSignature, $signatureHeader)) {
|
if (!$signatureHeader || !hash_equals($expectedSignature, $signatureHeader)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user