Update: 2026-06-16 17:47:17
This commit is contained in:
601
REMEDIATION_GUIDE.md
Normal file
601
REMEDIATION_GUIDE.md
Normal file
@@ -0,0 +1,601 @@
|
||||
# دليل الإصلاحات الشامل - مشروع سيرو
|
||||
|
||||
**التاريخ:** 16 يونيو 2026
|
||||
**المرحلة:** التطبيق العملي للإصلاحات
|
||||
**الحالة:** قيد التطوير
|
||||
|
||||
---
|
||||
|
||||
## النقطة 1️⃣: مشكلة البصمة الضعيفة (Weak Fingerprint Authentication)
|
||||
|
||||
### الملفات المتأثرة:
|
||||
|
||||
- `backend/login.php` (الراكب)
|
||||
- `backend/loginJwtDriver.php` (السائق)
|
||||
|
||||
### المشكلة الحقيقية:
|
||||
|
||||
```php
|
||||
// ❌ الحالة الحالية في login.php
|
||||
$fpVerified = hash_equals($storedFp, $fingerprint);
|
||||
// ✅ يحمي من timing attacks فقط
|
||||
// ⚠️ لكن البصمة نفسها ضعيفة وقابلة للاستخراج
|
||||
```
|
||||
|
||||
**لماذا ضعيفة؟**
|
||||
|
||||
- البصمة مستخرجة من جهاز المستخدم (ANDROID_ID + IMEI + MAC + Build.MODEL)
|
||||
- يمكن استخراجها عبر:
|
||||
- Frida/Objection أثناء التطبيق
|
||||
- ADB إذا كان USB debugging مفعل
|
||||
- مفاتيح الجهاز المُخزنة
|
||||
- بمجرد استخراج البصمة، يمكن تزويرها بنسخ نفس القيمة
|
||||
|
||||
### الحل: تطبيق MFA (Multi-Factor Authentication)
|
||||
|
||||
**المتطلبات:**
|
||||
|
||||
```
|
||||
عامل 1: بصمة الجهاز (الحالي) ✅
|
||||
عامل 2: OTP عبر SMS ← أضف هذا
|
||||
عامل 3: رمز الخادم (Server Token) ← أضف هذا
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## النقطة 2️⃣: مشكلة التشفير - IV الثابت
|
||||
|
||||
### الملف المتأثر:
|
||||
|
||||
- `backend/encrypt_decrypt.php` (الأساسي)
|
||||
|
||||
### المشكلة الفعلية:
|
||||
|
||||
```php
|
||||
// ❌ المشكلة الحرجة جداً
|
||||
$iv = getenv('initializationVector'); // 16 بايت ثابت!
|
||||
|
||||
public function encryptData($plainText) {
|
||||
$plainText = mb_convert_encoding($plainText, 'UTF-8');
|
||||
$paddedText = $this->addPadding($plainText);
|
||||
$encrypted = openssl_encrypt($paddedText, 'AES-256-CBC',
|
||||
$this->key, OPENSSL_RAW_DATA, $this->iv);
|
||||
// ↑ نفس IV في كل مرة = نفس ciphertext لنفس plaintext!
|
||||
return base64_encode($encrypted);
|
||||
}
|
||||
```
|
||||
|
||||
**مثال عملي:**
|
||||
|
||||
```
|
||||
التشفير الأول: "+20123456789" → "abc123xyz=="
|
||||
التشفير الثاني: "+20123456789" → "abc123xyz==" (متطابق!)
|
||||
⚠️ هجوم معروف - يمكن كسر التشفير تماماً
|
||||
```
|
||||
|
||||
### الحل: توليد IV عشوائي
|
||||
|
||||
```php
|
||||
✅ الحل الصحيح:
|
||||
|
||||
public function encryptData($plainText) {
|
||||
$plainText = mb_convert_encoding($plainText, 'UTF-8');
|
||||
$paddedText = $this->addPadding($plainText);
|
||||
|
||||
// توليد IV عشوائي لكل تشفير
|
||||
$randomIV = openssl_random_pseudo_bytes(16);
|
||||
|
||||
$encrypted = openssl_encrypt($paddedText, 'AES-256-CBC',
|
||||
$this->key, OPENSSL_RAW_DATA, $randomIV);
|
||||
|
||||
// ضمّ IV مع النص المشفر
|
||||
$result = $randomIV . $encrypted;
|
||||
|
||||
return base64_encode($result);
|
||||
}
|
||||
|
||||
public function decryptData($encryptedData) {
|
||||
$encrypted = base64_decode($encryptedData);
|
||||
|
||||
// استخرج IV من أول 16 بايت
|
||||
$iv = substr($encrypted, 0, 16);
|
||||
$ciphertext = substr($encrypted, 16);
|
||||
|
||||
$decrypted = openssl_decrypt($ciphertext, 'AES-256-CBC',
|
||||
$this->key, OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
return $this->removePadding($decrypted);
|
||||
}
|
||||
```
|
||||
|
||||
**الخطوات:**
|
||||
|
||||
1. توليد 16 بايت عشوائية → `openssl_random_pseudo_bytes(16)`
|
||||
2. تشفير البيانات بـ IV العشوائي
|
||||
3. ضمّ IV + Ciphertext
|
||||
4. تحويل إلى base64
|
||||
5. عند فك التشفير: استخرج IV + Ciphertext وفك التشفير
|
||||
|
||||
---
|
||||
|
||||
## النقطة 3️⃣: SQL Injection - الحالة الحالية وضح!
|
||||
|
||||
### الملف المتأثر:
|
||||
|
||||
- `backend/functions.php` (في `findBestDrivers()`)
|
||||
|
||||
### الحالة الحالية - ممتازة ✅
|
||||
|
||||
```php
|
||||
// ✅ هذا الكود **آمن تماماً** من SQL Injection
|
||||
$carType = trim($carType);
|
||||
$allowedCarTypes = [
|
||||
'Comfort', 'Mishwar Vip', 'Scooter', 'Pink Bike',
|
||||
'Electric', 'Lady', 'Van', 'Awfar Car', 'Fixed Price',
|
||||
'Speed', 'Rayeh Gai'
|
||||
];
|
||||
|
||||
// ✅ استخدام Allowlist (أفضل طريقة)
|
||||
if (!in_array($carType, $allowedCarTypes, true)) {
|
||||
$carType = 'Speed';
|
||||
}
|
||||
|
||||
// ✅ معاملات SQL آمنة
|
||||
$stmt = $con->prepare($sql);
|
||||
$stmt->execute($allParams);
|
||||
```
|
||||
|
||||
### لماذا هذا آمن؟
|
||||
|
||||
1. **Allowlist:** قائمة بيضاء للقيم المسموحة فقط
|
||||
2. **Prepared Statements:** معاملات آمنة
|
||||
3. **Type Strict (`true`):** التحقق الدقيق (`in_array(..., true)`)
|
||||
|
||||
### المشاكل المتبقية:
|
||||
|
||||
**نقاط أخرى قد تحتاج فحص:**
|
||||
|
||||
- `/backend/auth/` - هل تستخدم prepared statements؟
|
||||
- `/backend/ride/` - هل جميع الاستعلامات آمنة؟
|
||||
- `/walletintaleq.intaleq.xyz/v2/main/` - **حرج! تحتاج فحص كامل**
|
||||
|
||||
---
|
||||
|
||||
## النقطة 4️⃣: نظام المحفظة - أهم نقطة 🔴
|
||||
|
||||
### الملفات المتأثرة:
|
||||
|
||||
```
|
||||
/walletintaleq.intaleq.xyz/v2/main/ride/driverWallet/
|
||||
├── add.php ..................... 🔴 حرج - بلا مصادقة
|
||||
├── transfer.php ............... ⚠️ عالي
|
||||
├── update.php ................. ⚠️ عالي
|
||||
├── addFromAdmin.php ........... 🔴 حرج - مفتاح API ثابت
|
||||
├── get.php .................... ⚠️ عالي
|
||||
├── getWalletByDriver.php ...... ⚠️ عالي
|
||||
└── getDriverDetails.php ....... ⚠️ عالي
|
||||
```
|
||||
|
||||
### الفهم الحالي:
|
||||
|
||||
```
|
||||
تطبيق السائق
|
||||
↓
|
||||
POST /add ← يضيف الأموال
|
||||
↓
|
||||
/walletintaleq.intaleq.xyz/v2/main/ride/driverWallet/add.php
|
||||
↓
|
||||
← لا يوجد مصادقة! ❌
|
||||
```
|
||||
|
||||
### الحل المقترح: (S2S - Server to Server)
|
||||
|
||||
```
|
||||
تطبيق السائق
|
||||
↓
|
||||
POST /wallet/add (مع JWT توكن)
|
||||
↓
|
||||
BACKEND SERVER (backend/wallet/) ← نقطة جديدة
|
||||
↓
|
||||
• التحقق من JWT ✅
|
||||
• فحص الملكية ✅
|
||||
• التحقق من المبلغ ✅
|
||||
↓
|
||||
POST /v2/main/ride/driverWallet/add
|
||||
(مع توقيع HMAC-SHA256)
|
||||
↓
|
||||
WalletIntaleq Server
|
||||
↓
|
||||
• التحقق من التوقيع ✅
|
||||
• تنفيذ العملية ✅
|
||||
• تسجيل دقيق ✅
|
||||
```
|
||||
|
||||
### الخطوات العملية:
|
||||
|
||||
#### 1️⃣ إنشاء endpoint في Backend
|
||||
|
||||
```php
|
||||
// backend/wallet/add.php (جديد)
|
||||
|
||||
require_once __DIR__ . '/../core/bootstrap.php';
|
||||
|
||||
function requireAuth() {
|
||||
$headers = getallheaders();
|
||||
$authHeader = $headers['Authorization'] ?? '';
|
||||
|
||||
if (!preg_match('/Bearer\s+(\S+)/', $authHeader, $matches)) {
|
||||
http_response_code(401);
|
||||
jsonError('Authentication required');
|
||||
}
|
||||
|
||||
$token = $matches[1];
|
||||
$jwtService = new JwtService($redis);
|
||||
|
||||
try {
|
||||
$decoded = $jwtService->verifyAccessToken($token);
|
||||
return $decoded;
|
||||
} catch (Exception $e) {
|
||||
http_response_code(401);
|
||||
jsonError('Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$user = requireAuth(); // JWT verification
|
||||
|
||||
// التحقق من الدور (التفويض)
|
||||
if ($user->role !== 'driver') {
|
||||
http_response_code(403);
|
||||
jsonError('Permission denied');
|
||||
}
|
||||
|
||||
$driverID = $user->id;
|
||||
$amount = floatval(filterRequest('amount'));
|
||||
$source = filterRequest('source');
|
||||
|
||||
// التحقق من المبلغ
|
||||
if ($amount <= 0 || $amount > 10000) {
|
||||
jsonError('Invalid amount (max 10,000)', 400);
|
||||
}
|
||||
|
||||
// تحديد السرعة
|
||||
$limiter = new RateLimiter($redis);
|
||||
$limiter->enforce("wallet_add_{$driverID}", 'add', 1, 60); // 1 request per 60 seconds
|
||||
|
||||
// تسجيل التدقيق
|
||||
$auditLog = "Driver {$driverID} requested to add {$amount} from {$source}";
|
||||
securityLog($auditLog);
|
||||
|
||||
// الاتصال بـ WalletIntaleq عبر S2S
|
||||
$walletResponse = callWalletAPI('add', [
|
||||
'driver_id' => $driverID,
|
||||
'amount' => $amount,
|
||||
'source' => $source,
|
||||
'backend_request' => true,
|
||||
]);
|
||||
|
||||
if ($walletResponse['status'] === 'success') {
|
||||
jsonSuccess(['message' => 'تمت إضافة الأموال بنجاح']);
|
||||
} else {
|
||||
jsonError('Wallet operation failed', 500);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
securityLog("Wallet add error: " . $e->getMessage());
|
||||
jsonError('Internal error', 500);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2️⃣ دالة S2S API call آمنة
|
||||
|
||||
```php
|
||||
// backend/core/WalletConnector.php (جديد)
|
||||
|
||||
class WalletConnector {
|
||||
private $walletUrl = 'https://walletintaleq.intaleq.xyz/v2/main/';
|
||||
private $hmacSecret = null;
|
||||
|
||||
public function __construct() {
|
||||
$this->hmacSecret = getenv('WALLET_HMAC_SECRET');
|
||||
if (!$this->hmacSecret) {
|
||||
throw new Exception("WALLET_HMAC_SECRET not configured");
|
||||
}
|
||||
}
|
||||
|
||||
public function call($endpoint, $data) {
|
||||
// إضافة timestamp لمنع Replay Attacks
|
||||
$data['timestamp'] = time();
|
||||
$data['nonce'] = bin2hex(random_bytes(16));
|
||||
|
||||
// فرز البيانات وإنشاء signature
|
||||
ksort($data);
|
||||
$payload = json_encode($data);
|
||||
$signature = hash_hmac('sha256', $payload, $this->hmacSecret);
|
||||
|
||||
// إرسال الطلب
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $this->walletUrl . $endpoint,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'X-Signature: ' . $signature,
|
||||
'X-Timestamp: ' . $data['timestamp'],
|
||||
'X-Backend-ID: ' . getenv('BACKEND_ID'),
|
||||
],
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
// التحقق من الاستجابة
|
||||
$decoded = json_decode($response, true);
|
||||
if ($httpCode !== 200 || $decoded['status'] !== 'success') {
|
||||
throw new Exception("Wallet API error: " . $response);
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3️⃣ تعديل WalletIntaleq
|
||||
|
||||
```php
|
||||
// walletintaleq.intaleq.xyz/v2/main/ride/driverWallet/add.php
|
||||
|
||||
<?php
|
||||
// ✅ الآن يتلقى طلبات من Backend فقط
|
||||
|
||||
require_once __DIR__ . '/../../../core/bootstrap.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
// الحصول على البيانات المرسلة
|
||||
$payload = file_get_contents('php://input');
|
||||
$data = json_decode($payload, true);
|
||||
|
||||
// التحقق من التوقيع (HMAC)
|
||||
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
|
||||
$expectedSignature = hash_hmac('sha256', $payload, getenv('WALLET_HMAC_SECRET'));
|
||||
|
||||
if (!hash_equals($signature, $expectedSignature)) {
|
||||
http_response_code(401);
|
||||
jsonError('Invalid signature');
|
||||
}
|
||||
|
||||
// التحقق من الـ Timestamp (Replay Attack Prevention)
|
||||
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? 0;
|
||||
if (abs(time() - $timestamp) > 300) { // 5 دقائق
|
||||
http_response_code(401);
|
||||
jsonError('Request expired');
|
||||
}
|
||||
|
||||
// الآن آمن - نفذ العملية
|
||||
$driverID = $data['driver_id'];
|
||||
$amount = $data['amount'];
|
||||
$source = $data['source'];
|
||||
|
||||
// أضف إلى قاعدة البيانات
|
||||
$stmt = $con->prepare(
|
||||
"INSERT INTO driverWallet (driver_id, amount, source, created_at, updated_at)
|
||||
VALUES (?, ?, ?, NOW(), NOW())"
|
||||
);
|
||||
$stmt->execute([$driverID, $amount, $source]);
|
||||
|
||||
// تسجيل التدقيق
|
||||
securityLog("Wallet added: Driver $driverID, Amount $amount from $source");
|
||||
|
||||
jsonSuccess(['message' => 'Added successfully']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
securityLog("Wallet error: " . $e->getMessage());
|
||||
jsonError('Internal error', 500);
|
||||
}
|
||||
?>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## النقطة 5️⃣: أمان تطبيقات الهاتف - الأذونات
|
||||
|
||||
### الملف المتأثر:
|
||||
|
||||
- `siro_driver/android/app/src/main/AndroidManifest.xml`
|
||||
|
||||
### الأذونات الحالية:
|
||||
|
||||
```xml
|
||||
<!-- ✅ ضرورية لتطبيق السائق -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
|
||||
<!-- ⚠️ تحتاج فحص -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
|
||||
<!-- ✅ ضرورية للاتصالات -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
```
|
||||
|
||||
### الفحص المطلوب:
|
||||
|
||||
```bash
|
||||
1. بحث في codebase تطبيق السائق:
|
||||
grep -r "getExternalFilesDir\|getExternalCacheDir\|Environment.getExternalStorageDirectory" .
|
||||
|
||||
إذا لم تظهر نتائج → احذف WRITE/READ_EXTERNAL_STORAGE
|
||||
|
||||
2. بحث عن SYSTEM_ALERT_WINDOW:
|
||||
grep -r "TYPE_APPLICATION_OVERLAY\|canDrawOverlays" .
|
||||
|
||||
إذا لم تظهر → احذفها
|
||||
|
||||
3. بحث عن MODIFY_AUDIO_SETTINGS:
|
||||
grep -r "setVolume\|adjustStreamVolume" .
|
||||
|
||||
إذا لم تظهر → احذفها
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## النقطة 6️⃣: load_env.php - تحميل آمن للمتغيرات
|
||||
|
||||
### الملف:
|
||||
|
||||
- `backend/load_env.php`
|
||||
|
||||
### الحالة الحالية:
|
||||
|
||||
```php
|
||||
<?php
|
||||
function loadEnvironment($env_file) {
|
||||
if (!file_exists($env_file)) {
|
||||
error_log("❌ .env not found: $env_file");
|
||||
return false;
|
||||
}
|
||||
|
||||
$lines = file($env_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line) || strpos($line, '#') === 0) continue;
|
||||
|
||||
$parts = explode('=', $line, 2);
|
||||
if (count($parts) === 2) {
|
||||
[$keyName, $value] = $parts;
|
||||
$value = trim($value, "\"'");
|
||||
putenv("$keyName=$value");
|
||||
$_ENV[$keyName] = $value;
|
||||
$_SERVER[$keyName] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### القوة الأمنية:
|
||||
|
||||
✅ **جيد جداً:**
|
||||
|
||||
1. يتحقق من وجود الملف
|
||||
2. يتجاهل التعليقات (#)
|
||||
3. يتجاهل الأسطر الفارغة
|
||||
4. يزيل علامات الاقتباس
|
||||
5. يحمل في `$_ENV` و `$_SERVER`
|
||||
|
||||
### الطريقة الموصى بها (Composer autoload):
|
||||
|
||||
```json
|
||||
// composer.json - الأفضل
|
||||
|
||||
"autoload": {
|
||||
"files": ["src/bootstrap.php"]
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
// src/bootstrap.php
|
||||
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
|
||||
$dotenv->load();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## النقطة 7️⃣: backend/functions.php - الروابط الآمنة
|
||||
|
||||
### الحالة الحالية:
|
||||
|
||||
```php
|
||||
❌ غير آمنة:
|
||||
$url = "http://188.68.36.205:2021";
|
||||
$url = "http://188.68.36.205:3031";
|
||||
$url = "https://location.intaleq.xyz";
|
||||
```
|
||||
|
||||
### المشاكل:
|
||||
|
||||
1. **IP عام مكشوف** - 188.68.36.205
|
||||
2. **HTTP بدل HTTPS** - عرضة لـ MITM attacks
|
||||
3. **لا يوجد certificate pinning**
|
||||
|
||||
### الحل:
|
||||
|
||||
```php
|
||||
// ✅ الحل الآمن
|
||||
|
||||
function getAllowedSocketUrls(): array {
|
||||
return [
|
||||
'https://location.siromove.com', // ✅ HTTPS + Domain
|
||||
'https://socket.siromove.com', // ✅ HTTPS + Domain
|
||||
// لا HTTP
|
||||
];
|
||||
}
|
||||
|
||||
function sendToLocationServer($action, $data) {
|
||||
$url = "https://location.siromove.com/api";
|
||||
|
||||
// تثبيت الشهادة (Certificate Pinning)
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_POST => 1,
|
||||
CURLOPT_POSTFIELDS => http_build_query($data),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
|
||||
// ✅ تأمين SSL/TLS
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_CAINFO => '/etc/ssl/certs/ca-bundle.crt',
|
||||
|
||||
// ✅ Certificate Pinning (اختياري لكن موصى)
|
||||
// تحتاج حساب SHA-256 للشهادة
|
||||
CURLOPT_PINNEDPUBLICKEY => 'sha256/AAAA....',
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ملخص الإصلاحات
|
||||
|
||||
| النقطة | المشكلة | الحل | المدة |
|
||||
| ------ | ------------------ | ------------- | -------- |
|
||||
| 1 | بصمة ضعيفة | MFA + OTP | 8 ساعات |
|
||||
| 2 | IV ثابت | توليد عشوائي | 4 ساعات |
|
||||
| 3 | SQL Injection | آمن بالفعل ✅ | 0 ساعات |
|
||||
| 4 | المحفظة بلا مصادقة | S2S + JWT | 16 ساعات |
|
||||
| 5 | أذونات مفرطة | تقليص + فحص | 4 ساعات |
|
||||
| 6 | load_env.php | آمن بالفعل ✅ | 0 ساعات |
|
||||
| 7 | روابط HTTP | تبديل HTTPS | 2 ساعات |
|
||||
|
||||
**المجموع:** ~34 ساعة
|
||||
|
||||
---
|
||||
|
||||
## التالي:
|
||||
|
||||
1. تطبيق الإصلاحات واحدة تلو الأخرى
|
||||
2. اختبار كل إصلاح
|
||||
3. ترحيل قاعدة البيانات
|
||||
4. نشر للإنتاج
|
||||
Reference in New Issue
Block a user