الفرق الحمراء — الهجومخبير80mL32

Angular Security — Sanitizer مش حصن

Trusted Types و DomSanitizer bypasses و AOT vs JIT و SSR leaks

#Angular#Sanitizer#Template#Trusted Types#SSR

Angular مختلف من جذره

Angular مش زي React في الموضوع ده.

Angular بيفرض contextual sanitization by default — كل قيمة بتدخل الـ DOM، Angular بيعرف هي رايحة فين (HTML, URL, Style, Script, Resource URL) وبيعقمها على الأساس ده.

طب فين الخرّامة؟

في الأبواب الجانبية اللي Angular نفسه فاتحها: bypassSecurityTrust*.

تشبيه — شرح مبسط
قفل ذكي بيميّز المفاتيح المسجّلة عنده، وبيرفض أي مفتاح تاني. بس صاحب البيت بإيده يقدر يحط علامة "trust" على أي مفتاح. اليوم اللي يحط فيه العلامة على مفتاح غريب، الحماية انتهت.

- طب يا حضرتك، Angular أمن من React.. إيه اللي يخليني أقلق؟

يا مستجد. Angular فعلاً بيدّيك حماية افتراضية أحسن. بس بيدّيك في نفس الوقت باب جانبي اسمه واضح: bypassSecurityTrustHtml. والمبرمجين بيستخدموه عشان "الـ warning يسكت". اليوم اللي حد كتب السطر ده من غير ما يفهمه — ده اليوم اللي الحماية فيه راحت.
بُص بقى — bypassSecurityTrust في كل codebase تقريباً
لو تـ grep على أي codebase Angular كبير، هتلاقي 20-50 استخدام لـ bypassSecurityTrustHtml. 80% منهم في غير محله. "كنا محتاجين نـ render markdown" — تمام، بس بعد DOMPurify. "كنا محتاجين iframe من YouTube" — يبقى استخدم bypassSecurityTrustResourceUrl على whitelist محددة. المبرمج بيكتبها مرة، بتنجح، بينساها. وبعد سنة الـ XSS بيدخل من نفس السطر.
تذكير قانوني
الأمثلة دي لتعليم XSS والـ bypasses جوه تطبيقاتك إنت. ما تطبّقهاش على تطبيق مش بتاعك.

الـ DomSanitizer — بيشتغل إزاي وبيتخطى إزاي

Angular بيصنف القيم لـ 5 سياقات:

HTML
المحتوى داخل element. مثال: [innerHTML].
STYLE
قيمة CSS. مثال: [style.background]. يحظر expression(), javascript:.
URL
الروابط. مثال: [href]. يحظر javascript:, data:.
RESOURCE_URL
Scripts, iframes. مثال: [src] لـ iframe. الأشد صرامة.
SCRIPT
script.text. يحظر بالكامل.
typescript
// خطر — bypass كامل
import { DomSanitizer } from '@angular/platform-browser';

@Component({
  template: `<div [innerHTML]="content"></div>`
})
export class CmpComponent {
  content: SafeHtml;
  constructor(private sanitizer: DomSanitizer) {}
  load(userInput: string) {
    this.content = this.sanitizer.bypassSecurityTrustHtml(userInput);
    // !! لا توجد حماية إطلاقاً !!
  }
}

// PoC: userInput = "<img src=x onerror=alert(document.cookie)>"

// الإصلاح
load(userInput: string) {
  // إما تستخدم interpolation {{ userInput }} (يهرّب تلقائياً)
  // أو DOMPurify مع SafeHtml
  this.content = this.sanitizer.bypassSecurityTrustHtml(
    DOMPurify.sanitize(userInput)
  );
  // أو تتجنب innerHTML تماماً
}
القاعدة الذهبية
كل bypassSecurityTrust* هو code smell. كل سطر منهم لازم له: مبرر مكتوب + sanitization منفصلة + code review.

Template Injection — خاصة بـ Angular

الـ Angular template هو expression language. لو خدت template من إنت user input وعملت له compile، إنت كدة بتنفذ كود مباشرة.

typescript
// خطر — Server-Side Template Injection
// تطبيق يولّد components من DB (CMS-like)
@Component({
  template: `${userInputFromCMS}`,   // !! template أحياناً يحوي JS expressions
})

// PoC في Angular template:
{{constructor.constructor('alert(1)')()}}
// أو
{{$any(1).constructor.constructor('return process')()}}    // SSR escape

// JIT compilation runtime (التطبيق غير AOT)
@Component({
  selector: 'dynamic',
  template: this.userTemplate,    // ← compile + run user code
})

// دفاع — استخدم AOT compilation فقط (default في prod من Angular 9+)
// ng build --aot
// JIT compilation معطّل في prod

bypassSecurityTrustResourceUrl — أخطر فخ في Angular

typescript
// خطر — iframe يأخذ URL من user
@Component({
  template: `<iframe [src]="vidUrl"></iframe>`
})
export class VideoCmp {
  vidUrl: SafeResourceUrl;
  constructor(private s: DomSanitizer) {}

  setVideo(url: string) {
    this.vidUrl = this.s.bypassSecurityTrustResourceUrl(url);
  }
}

// PoC: setVideo("javascript:alert(document.cookie)")
// أو data:text/html,<script>...

// الإصلاح
setVideo(url: string) {
  // allowlist صارمة
  const u = new URL(url);
  if (u.hostname !== 'youtube.com' || !/^\/embed\//.test(u.pathname)) {
    return;
  }
  this.vidUrl = this.s.bypassSecurityTrustResourceUrl(u.toString());
}

SSR — Angular Universal

Angular Universal بيشغل الكود على Node.js عشان الـ SSR. كل ثغرات Node بتنطبق هنا، وبتيجي معاها ثغرات خاصة بالـ rendering:

  • Hydration mismatch — زي React، بيكشف الـ server-side state.
  • SSR XSS — لو الكود بيستخدم document.write أو بيلعب في الـ DOM في context السيرفر، الـ user input ممكن يدخل من غير escape.
  • Resource leaks — كل request بينشئ Angular module جديد. شغل CPU ثقيل على demand = السيرفر بيقع.
  • Secrets exposure — الـ initial state بينطبع جوه الـ HTML (TransferState). أي حاجة fetched قبل ما الـ client يستلم = ظاهرة في الـ source.
typescript
// خطر — TransferState يحوي بيانات حسّاسة
constructor(
  private state: TransferState,
  private http: HttpClient
) {
  const KEY = makeStateKey<User>('user-data');
  this.user = this.state.get(KEY, null);
  if (!this.user) {
    this.http.get<User>('/api/me').subscribe(u => {
      this.state.set(KEY, u);   // !! يُطبع في HTML
    });
  }
}

// User يحوي email, internalRoles, ...
// كل visitor يقرأ HTML source يرى البيانات

// آمن — DTO فقط للـ TransferState، لا entities كاملة
const safeUser = { id: u.id, name: u.name };
this.state.set(KEY, safeUser);

Trusted Types — Angular بيدعمها بقوة

Angular 16+ بيدعم Trusted Types ببساطة. مع CSP require-trusted-types-for 'script'، أي bypass بيتلغي على مستوى المتصفح نفسه.

typescript
// app.module.ts أو main.ts
import { TrustedTypesModule } from '@angular/platform-browser';

// CSP header
// Content-Security-Policy: require-trusted-types-for 'script'; trusted-types angular angular#unsafe-bypass;

// لو حاول كود في bundle أن يضع innerHTML بـ string عادي → throw
// فقط Angular sanitizer (الذي ينتج TrustedHTML) يعمل
ميزة Angular
على عكس React، Angular مصمم حوالين Trusted Types — التفعيل سهل والفايدة فورية. لازم يكون default في أي deploy إنتاجي جديد، مفيش كلام.

Routing — Open Redirect وRoute Guards

typescript
// خطر — redirect بعد login
loginCmp() {
  this.auth.login(...).subscribe(() => {
    const next = this.route.snapshot.queryParams['returnUrl'];
    this.router.navigateByUrl(next);   // attacker controls
  });
}

// PoC: /login?returnUrl=//evil.com

// آمن
loginCmp() {
  this.auth.login(...).subscribe(() => {
    let next = this.route.snapshot.queryParams['returnUrl'] || '/';
    if (!next.startsWith('/') || next.startsWith('//')) next = '/';
    this.router.navigateByUrl(next);
  });
}

// Route Guards — احرص على CanActivate و CanActivateChild
const routes: Routes = [
  {
    path: 'admin',
    canActivate: [AdminGuard],
    canActivateChild: [AdminGuard],   // ← لا تنسَ children!
    children: [...]
  }
];

// AdminGuard يعتمد على service موثوق، ليس localStorage مباشرة
@Injectable({ providedIn: 'root' })
export class AdminGuard implements CanActivate {
  constructor(private auth: AuthService, private router: Router) {}
  canActivate(): boolean {
    if (!this.auth.isAdmin()) {
      this.router.navigate(['/forbidden']);
      return false;
    }
    return true;
  }
}

HttpClient — XSRF والـ Interceptors

Angular معاه حماية CSRF مدمجة عن طريق HttpClientXsrfModule. بيقرا الـ cookie اسمه XSRF-TOKEN ويبعته في header X-XSRF-TOKEN.

typescript
@NgModule({
  imports: [
    HttpClientModule,
    HttpClientXsrfModule.withOptions({
      cookieName: 'XSRF-TOKEN',
      headerName: 'X-XSRF-TOKEN',
    })
  ]
})

// الخادم يضبط cookie XSRF-TOKEN عند login
// يرفض كل non-GET بدون header X-XSRF-TOKEN

// خطر شائع — Interceptor يضيف Authorization من localStorage
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const token = localStorage.getItem('token');   // XSS = سرقة
    return next.handle(req.clone({
      setHeaders: { Authorization: `Bearer ${token}` }
    }));
  }
}

// أفضل: cookie httpOnly + إعادة الـ XSRF token في كل non-GET

Forms — الـ Validation والـ Reactive Forms

  • Server-side validation إجباري — Angular validators ديكور بس، أي حد بيتخطاهم بـ DevTools في ثواني.
  • FormControl values — أي one-way binding من user input للـ model، احتفظ بنسخة DTO مفلترة.
  • Custom async validators — متستخدمش eval ولا كود ديناميكي جاي من API.
  • File upload — زي React/Express: افحص MIME + الحجم + الـ extension + اعمل scan.

JIT vs AOT — ليه الـ AOT حماية؟

  • JIT — الـ templates بتتجمع runtime في المتصفح. أبطأ + بتفتح باب لـ template injection.
  • AOT — الـ templates بتتجمع وقت الـ build. أسرع + بتمسك أخطاء الـ template بدري، والـ templates الديناميكية بتبقى مستحيلة.
  • من Angular 9 (Ivy)، الـ AOT default. اتأكد إن ng build --configuration=production بيستخدم AOT (وهو فعلاً بيستخدمه).
  • ابعد عن @Component({ jit: true }) وJitCompilerFactory في prod.

Supply chain — منظومة Angular

  • npm + @angular/* — حدّث Angular بانتظام. إصدار major كل 6 شهور، LTS سنة كاملة.
  • Schematics من طرف تالتng add بينفذ كود. اتأكد من المصدر قبل أي حاجة.
  • Material / PrimeNG / NG-Zorro — مكتبات UI ضخمة وعندها تاريخ CVEs (XSS في tooltip وpopover). حدّث.
  • SystemJS / esm.sh في الـ dev — متشغلهاش في prod أبداً.

CSP صارم لـ Angular

text
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{rand}';   ← Angular لا يحتاج unsafe-inline في AOT
  style-src 'self' 'unsafe-inline';   ← ngStyle يستخدم inline
  img-src 'self' data: https:;
  font-src 'self' data:;
  connect-src 'self' https://api.target.gov;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  require-trusted-types-for 'script';
  trusted-types angular;
ملاحظة CSP
لو استخدمت DomSanitizer.bypassSecurityTrustStyle، يمكن تحتاج 'unsafe-inline' في style-src. حاول تستخدم classes جاهزة بدل inline style أصلاً.

Checklist مراجعة Angular app

  1. كل bypassSecurityTrust* معاه justification + DOMPurify.
  2. كل [innerHTML] جاي من مصدر موثوق أو ماشي عبر sanitization.
  3. AOT في prod (افحص angular.json).
  4. HttpClientXsrfModule مفعل + السيرفر بيتحقق من X-XSRF-TOKEN.
  5. كل route admin/protected معاه CanActivate وCanActivateChild.
  6. Trusted Types CSP مفعل.
  7. مفيش tokens في localStorage — httpOnly cookies بس.
  8. TransferState مفيهوش PII ولا internal flags.
  9. SSR (Universal): timeouts على الـ HTTP، مفيش synchronous heavy work.
  10. Open redirect filter على كل navigateByUrl جاي من user input.
  11. Forms: server-side validation كاملة، متثقش في Angular validators أبداً.
  12. Material/PrimeNG محدّث، الـ CVEs متحلولة.
أدوات
@angular-eslint, eslint-plugin-security, Snyk, Semgrep p/angular, Sonarqube مع Angular ruleset.

غلطات الـ junior في Angular

الأخطاء اللي بتفتح الباب
  • bypassSecurityTrustHtml لكل markdown — من غير DOMPurify. الـ XSS بيدخل بالباب اللي إنت فاتحه.
  • JIT في production — أبطأ، أكبر، وفيه class من template injection ما بتطلعش في AOT. اشتغل AOT دايماً.
  • HttpClient من غير CSRF token — Angular بيدّيك HttpClientXsrfModule مجاناً. استخدمه.
  • Tokens في localStorage — Angular مش بيفرق عن React هنا. HttpOnly cookies + SameSite.
  • Route guards بس على parent route — الـ child routes مفتوحة. CanActivateChild ضرورة.
  • TransferState بـ PII — الـ SSR بيـ embed البيانات في HTML للـ hydration. أي حد بيـ view source يلاقيها.
  • navigateByUrl بـ user input — open redirect مفتوح. لازم whitelist.

الخلاصة الناشفة

Angular بيدّيك أمان افتراضي أحسن من React. وفي نفس الوقت بيدّيك أبواب جانبية اسمها واضح: bypassSecurityTrust*.

الفرق بين Angular آمن وApp مكشوف = إنت فاهم ليه استخدمت كل bypass في الكود، ولا حطّيتها عشان TypeScript يسكت.

اقرا الـ bypassSecurityTrust* في codebase بتاعك. لو فيه واحد ما عندوش سبب مكتوب جنبه، يبقى دي ثغرتك القادمة.

اكتبها على ظهر إيدك:

كل bypassSecurityTrust* من غير تعليق بيشرح ليه = thread review قادم. اوعى تنسى.