From d2a34f753c8f7837ea90288036e76819a69e30e7 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 17 Jul 2026 16:36:14 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20TLS=20=E2=80=94=20=D8=A5=D8=BA=D9=84?= =?UTF-8?q?=D8=A7=D9=82=20=D8=A7=D9=84=D9=85=D9=86=D8=A7=D9=81=D8=B0=20?= =?UTF-8?q?=D8=A7=D9=84=D9=85=D9=83=D8=B4=D9=88=D9=81=D8=A9=20+=20=D8=A7?= =?UTF-8?q?=D9=84=D8=AB=D9=82=D8=A9=20=D8=A8=D8=A7=D9=84=D8=A8=D8=B1=D9=88?= =?UTF-8?q?=D9=83=D8=B3=D9=8A=20+=20=D8=AF=D9=84=D9=8A=D9=84=20=D8=A7?= =?UTF-8?q?=D9=84=D8=A5=D8=B5=D8=AF=D8=A7=D8=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit النطاق: tripz-api.intaleqapp.com (قرار المالك) — مملوك وجاهز، ومحايد عن اسم العلامة الذي لم يُحسم فلا يحتاج تغييراً لاحقاً. ثغرتان كانتا ستُبطلان TLS من أصله: - API كان منشوراً على 0.0.0.0:4010 → صار 127.0.0.1. بلا هذا يبقى http://IP:4010 مفتوحاً فيتجاوز TLS كلياً وتصير الشهادة زينة - **Postgres كان مكشوفاً على 0.0.0.0:55432** — القاعدة على الإنترنت مباشرة → صار 127.0.0.1. المنفذ غير القياسي إخفاء لا أمان. الوصول من الماك عبر نفق SSH (docs/20 §4) trust proxy = 1: خلف Nginx كان req.ip يصير 127.0.0.1 للجميع فيمتلئ سجل التدقيق المالي (I7) بعنوان البروكسي بدل السائق — أثرٌ بلا قيمة عند النزاع. قفزة واحدة فقط: الثقة المفتوحة تسمح للعميل بتزوير X-Forwarded-For. docs/20: DNS → موقع reverse proxy → شهادة → **إعداد ترقية WebSocket** (بدونه يسقط التتبّع الحيّ) + مهلة 3600s للنبضات طويلة العمر + فحص تحقّق أهمّه أن http://IP:4010 يجب أن **يفشل**. Co-Authored-By: Claude Opus 4.8 --- backend/.env.example | 3 +- backend/docker-compose.yml | 10 ++++- backend/src/main.ts | 9 +++- docs/17-backend-backlog.md | 12 +++-- docs/20-tls.md | 91 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 docs/20-tls.md diff --git a/backend/.env.example b/backend/.env.example index 0eefaa0..719b442 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -2,7 +2,8 @@ # ملاحظة: السيرفر مشترك — كل شيء معزول ببادئة tripz و Redis DB غير الافتراضي (راجع docs/14) NODE_ENV=development -API_PORT=4010 # منفذ غير شائع لتفادي التصادم على السيرفر المشترك +API_PORT=4010 # يُنشر على 127.0.0.1 فقط — Nginx (CloudPanel) ينهي الـTLS. راجع docs/20 +DB_EXPOSE_PORT=55432 # 127.0.0.1 فقط. للوصول من الماك: نفق SSH (docs/20 §4) # ---- PostgreSQL (قاعدة مستقلة + بادئة جداول) ---- DB_HOST=postgres diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 61d89ca..e08dd1d 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -16,7 +16,10 @@ services: volumes: - tripz-pgdata:/var/lib/postgresql/data ports: - - "${DB_EXPOSE_PORT:-55432}:5432" # منفذ مضيف غير قياسي (عزل) + # 127.0.0.1 فقط — كانت مكشوفة على 0.0.0.0 أي أن القاعدة كانت على + # الإنترنت مباشرة. المنفذ غير القياسي ليس أماناً، هو إخفاء. + # للوصول من الماك: نفق SSH (راجع docs/20). + - "127.0.0.1:${DB_EXPOSE_PORT:-55432}:5432" networks: [tripz-net] redis: @@ -50,7 +53,10 @@ services: command: ["node", "dist/main.js"] depends_on: [postgres, redis] ports: - - "${API_PORT:-4010}:4010" + # 127.0.0.1 فقط: لا يصل إليه إلا Nginx المحلي (CloudPanel) الذي ينهي + # الـTLS. بلا هذا يبقى http://IP:4010 مفتوحاً فيتجاوز الـTLS كله + # ويصير الشهادة زينة (docs/20). + - "127.0.0.1:${API_PORT:-4010}:4010" volumes: - tripz-storage:/app/storage networks: [tripz-net] diff --git a/backend/src/main.ts b/backend/src/main.ts index d612e73..96e7bde 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -3,13 +3,14 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { NestExpressApplication } from '@nestjs/platform-express'; import { AppModule } from './app.module'; import { RedisIoAdapter } from './realtime/redis-io.adapter'; async function bootstrap() { // rawBody: التوقيع يُحسب على الجسم **كما وصل** (docs/17 — I6). إعادة تسلسل // JSON قد تغيّر ترتيب المفاتيح أو المسافات فيفشل توقيع سليم. - const app = await NestFactory.create(AppModule, { rawBody: true }); + const app = await NestFactory.create(AppModule, { rawBody: true }); const cfg = app.get(ConfigService); // مُحوّل Socket.IO عبر Redis — يفعّل التوسّع الأفقي (عدة نسخ) @@ -17,6 +18,12 @@ async function bootstrap() { await redisIoAdapter.connectToRedis(); app.useWebSocketAdapter(redisIoAdapter); + // خلف بروكسي (Nginx ينهي الـTLS): بلا هذا يصير req.ip = 127.0.0.1 للجميع، + // فيمتلئ سجل التدقيق المالي (docs/17 — I7) بعنوان البروكسي بدل عنوان + // السائق — أي أثرٌ بلا قيمة عند النزاع. `1` = ثقة بقفزة واحدة فقط؛ الثقة + // المفتوحة تسمح للعميل بتزوير X-Forwarded-For. + app.set('trust proxy', 1); + app.setGlobalPrefix('api'); app.useGlobalPipes( new ValidationPipe({ whitelist: true, transform: true }), diff --git a/docs/17-backend-backlog.md b/docs/17-backend-backlog.md index 108596a..7e2192e 100644 --- a/docs/17-backend-backlog.md +++ b/docs/17-backend-backlog.md @@ -176,10 +176,14 @@ | I3 | **جدول لكل طريقة دفع** | ⏳ لم يُنفَّذ — إعادة هيكلة مخطط كاملة (`tripz_pay_payments` عام حالياً). يُنفَّذ مع أول ربط بوابة حقيقية، لا قبله. | | I8 | **webhook SMS + Gemini** | ⏳ لم يُنفَّذ — ميزة كاملة (`raw_sms_log` + استخراج + تسوية). تُبنى عند دخول السوق السوري فعلياً. | -### 🔴 أخطر ثغرة مالية قائمة — ليست في الكود -**الـAPI يعمل على `http` بلا TLS** (`194.163.173.157:4010`، والتطبيق يفعّل `usesCleartextTraffic=true`). من يلتقط الشبكة يسرق توكن أي سائق ويسحب أرباحه — ولا OTP ولا HMAC ولا تدقيق يمنع ذلك تماماً. -- توقيع I6 **يخفّف** الضرر (التوكن المسروق وحده لا يكفي: المفتاح لا يمرّ في الشبكة بعد الدخول) لكنه **ترقيع لا بديل**. -- **الإصلاح الحقيقي: TLS** (نطاق + شهادة عبر CloudPanel/Let's Encrypt) ثم `usesCleartextTraffic=false`. **يسبق أي عمل مدفوعات آخر.** +### 🔴 TLS — أخطر ثغرة مالية كانت قائمة. **المستند: [20-tls](20-tls.md)** +الـAPI كان على `http` بلا TLS: من يلتقط الشبكة يسرق توكن أي سائق ويسحب أرباحه، ولا OTP ولا HMAC ولا تدقيق يمنع ذلك. +**النطاق المعتمد: `tripz-api.intaleqapp.com`.** الجزء البرمجي ✅ (المنافذ + trust proxy)؛ الباقي خطوات سيرفر يدوية في docs/20. + +وأثناء العمل ظهرت ثغرتان تُبطلان TLS من أصله لو تُركتا: +- **`4010` كان مكشوفاً على `0.0.0.0`** → صار `127.0.0.1`. بلا هذا يبقى `http://IP:4010` مفتوحاً فيتجاوز TLS كلياً والشهادة زينة. +- **Postgres كان مكشوفاً على `0.0.0.0:55432`** — أي القاعدة على الإنترنت مباشرة → صار `127.0.0.1` (الوصول من الماك عبر نفق SSH). +- **`trust proxy = 1`**: خلف Nginx كان `req.ip` سيصير `127.0.0.1` للجميع فيمتلئ سجل التدقيق المالي (I7) بعنوان البروكسي بدل السائق — أثرٌ بلا قيمة عند النزاع. والقفزة الواحدة مقصودة: الثقة المفتوحة تسمح بتزوير `X-Forwarded-For`. --- diff --git a/docs/20-tls.md b/docs/20-tls.md new file mode 100644 index 0000000..02dd54c --- /dev/null +++ b/docs/20-tls.md @@ -0,0 +1,91 @@ +# 20 — TLS: إنهاء الطبقة المشفّرة أمام الـAPI + +> **النطاق المعتمد: `tripz-api.intaleqapp.com`** (قرار المالك 2026-07-17). +> السبب في اختيار نطاق فرعي على `intaleqapp.com`: مملوك وجاهز بلا انتظار DNS، ومحايد عن اسم العلامة التجارية الذي لم يُحسم — فلا يحتاج تغييراً لاحقاً. + +--- + +## 1. لماذا هذا يسبق كل عمل مدفوعات +الـAPI كان يعمل على `http://194.163.173.157:4010`. من يلتقط الشبكة (واي-فاي عام) يقرأ توكن أي سائق ويسحب أرباحه. **لا OTP ولا HMAC ولا سجل تدقيق يمنع ذلك** — كلها تُبنى فوق قناة مكشوفة. TLS هو الأساس، لا طبقة إضافية. + +## 2. ثغرتان أُصلحتا في نفس الشغل (بلا هذا يصير TLS زينة) +| كان | صار | لماذا | +|-----|-----|--------| +| `4010:4010` أي `0.0.0.0` | `127.0.0.1:4010` | بلا هذا يبقى `http://IP:4010` مفتوحاً **فيتجاوز TLS كلياً**. الشهادة على النطاق لا تُغلق المنفذ. | +| `55432:5432` أي `0.0.0.0` | `127.0.0.1:55432` | **Postgres كان على الإنترنت مباشرة.** المنفذ غير القياسي إخفاء لا أمان. | + +بعد هذا لا يصل للـAPI إلا Nginx المحلي، ولا تصل القاعدة إلا من داخل السيرفر. + +## 3. الخطوات على السيرفر + +### 3.1 DNS +سجل `A` لـ`tripz-api.intaleqapp.com` → `194.163.173.157`. تحقّق قبل المتابعة: +```bash +dig +short tripz-api.intaleqapp.com # يجب أن يطبع 194.163.173.157 +``` +> Let's Encrypt يفشل إن لم ينتشر الـDNS بعد. لا تُصدر الشهادة قبل أن يطبع الأمر العنوان الصحيح. + +### 3.2 موقع Reverse Proxy في CloudPanel +CloudPanel → **Add Site** → **Create Reverse Proxy**: +- Domain: `tripz-api.intaleqapp.com` +- Reverse Proxy URL: `http://127.0.0.1:4010` + +### 3.3 الشهادة +CloudPanel → الموقع → **SSL/TLS** → **Actions → New Let's Encrypt Certificate**. +CloudPanel يجدّدها تلقائياً. + +### 3.4 دعم WebSocket (إلزامي — بدونه يسقط التتبّع الحيّ) +Socket.IO يحتاج ترقية الاتصال. في **Vhost Editor** للموقع، تأكّد أن كتلة `location /` تحوي: +```nginx +proxy_http_version 1.1; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "upgrade"; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; + +# نبضات المواقع والسوكت طويلة العمر — الافتراضي 60s يقطعها +proxy_read_timeout 3600s; +proxy_send_timeout 3600s; +``` +`X-Forwarded-For` **ضروري**: بدونه يسجّل التدقيق المالي (docs/17 — I7) عنوان البروكسي لكل سائق، فيصير الأثر بلا قيمة. الباك إند يثق بقفزة واحدة فقط (`trust proxy = 1`) — الثقة المفتوحة تسمح للعميل بتزوير العنوان. + +### 3.5 النشر +```bash +cd /home/tripz-llc && git pull origin main +cd backend && docker compose up -d --build +docker image prune -f +``` + +### 3.6 التحقق +```bash +curl -I https://tripz-api.intaleqapp.com/api/health # 200 عبر TLS +curl -I http://194.163.173.157:4010/api/health # يجب أن **يفشل** الآن +docker compose exec api printenv | grep -c PLATFORM_SECRET # 1 +``` +**الفحص الثاني هو الأهم**: نجاحه يعني أن المنفذ ما زال مكشوفاً وأن TLS يُتجاوَز. + +## 4. الوصول للقاعدة بعد الإغلاق +لم تعد `55432` مفتوحة. من الماك: +```bash +ssh -L 55432:127.0.0.1:55432 root@194.163.173.157 +# ثم اتصل بـ localhost:55432 كالمعتاد +``` +سكربتات التحقق لا تتأثر — تعمل داخل شبكة `tripz-net` وتنادي `http://tripz-api:4010` مباشرة. + +## 5. بعد نجاح TLS — خطوات تابعة +| البند | التفصيل | +|-------|---------| +| **فلاتر: BASE_URL** | `--dart-define BASE_URL=https://tripz-api.intaleqapp.com/api` | +| **فلاتر: إغلاق النصّ الصريح** | `usesCleartextTraffic=false` في `AndroidManifest` — يمنع أي رجوع صامت لـhttp | +| **تفعيل التوقيع** | `PAYMENTS_REQUIRE_SIGNATURE=true` بعد أن يوقّع فلاتر (docs/17 — I6) | +| **HSTS** | بعد التأكد من استقرار TLS — يمنع الرجوع لـhttp | +| **بقية `intaleqapp.com`** | `map-saas` و`nabeh` على https أصلاً ✅ | + +## 6. ما لا يحلّه TLS +- **لا يحمي من تطبيق مُفكَّك**: TLS يحمي القناة لا الطرف. الحماية من الطرف هي الاستحقاقات على السيرفر ([19-entitlements-licensing](19-entitlements-licensing.md)). +- **لا يمنع pinning bypass**: من يملك الجهاز يرى حركته. لذلك تبقى القاعدة: **السيرفر لا يثق بالعميل أبداً**. + +--- +← ذو صلة: [15-deploy-flow](15-deploy-flow.md) · [14-server-conventions](14-server-conventions.md) · [17-backend-backlog](17-backend-backlog.md)