TL;DR
- Kullanıcı destek ürünleri için gömülebilir bir destek widget’ı oluşturuldu: çerez yok — kısa ömürlü bir bearer token, başlıkta gönderiliyor ve dinleniyor.
- Giriş, ana sayfadan HMAC imzalı bir beyan. Lansmandan önce bir düşman değerlendirmesi, dört gerçek güvenlik açığını buldu.
- Dışarıya gönderilen webhooks: tam byte’ları imzalayın, idempotency için dedupe anahtarı, hedef URL’lerinde SSRF koruması.
Gereksinim: Çalışanlar, ürünün sahibi olmadığı sayfalardan bilet açabiliyor. Bu, gömülebilir bir widget anlamına geliyor — ve gömülebilir olmak, oturumlar hakkında bildiğiniz her şeyi durdurur.
<h2><a name="why-cookiefree" href="#why-cookiefree"></a>Why cookie-free</h2>
<p>Widget, müşterilerin alan adlarında yaşamaktadır, bu nedenle ayarladığı herhangi bir çerez üçüncü taraf çerezi olur — modern tarayıcılar tarafından engellenir veya bölümlere ayrılır. Bununla savaşmak istiyorsanız, oturumlar tutarsız olur, bu yüzden hiç çerez yok. Giriş değişimi, widget'ın bir başlıkta gönderdiği kısa ömürlü bir oturum token'ı oluşturur ve sunucu, oturumu <code>sha256(token)</code> ile anahtarlandırarak önbelleğe alır — bir önbellek dökümü, tekrar oynatılabilir bir şey vermez. Oturumlar 60 dakika sürer ve süre aşımı, UI'da gerçek bir kurtarma yolunu gösterir.<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>customer backend widget (on customer page) helpdesk API| signs ref|email|name | |
| into HMAC assertion —> |– redeem assertion (single use) ->|
| ||
<h2><a name="what-the-adversarial-review-caught" href="#what-the-adversarial-review-caught"></a>What the adversarial review caught</h2>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Finding</th>
<th>Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Replay burn keyed by client-chosen nonce</td>
<td>Burn by HMAC signature — a leaked assertion can't mint extra sessions</td>
</tr>
<tr>
<td>`\`</td>
<td>` accepted inside signed fields</td>
</tr>
<tr>
<td>Origin check failed open when Origin/Referer absent</td>
<td>Fall back to the unspoofable <code>Sec-Fetch-Dest</code> header to enforce embedding</td>
</tr>
<tr>
<td>Widget could request critical severity</td>
<td>Clamp effective severity (including the channel default) to the widget's allowlist</td>
</tr>
</tbody>
</table>
</div>
<p>Favori bulgum, ayırıcı olanıdır. Eğer <code>ref|email|name</code> imzanız var ve bir alanda <code>|</code> kabul ederseniz, iki farklı kimlik demeti bir geçerli imza paylaşabilir. Kanonikleştirme hataları, kriptografik hatalar değildir.</p>
<h2><a name="webhooks-out-sign-the-exact-bytes" href="#webhooks-out-sign-the-exact-bytes"></a>Webhooks out: sign the exact bytes</h2>
<p>Dışarıya gönderilen webhooks, sıraya alındıklarında bir kez kompoze edilir ve saklanır; teslimat işi, saklanan yükü sabit JSON bayraklarıyla yeniden kodlar ve bu tam byte'ları imzalar. Yüklenimden sonra, yük dejenere olamaz, böylece yeniden serileştirmeden kaynaklanan imza uyuşmazlıkları ortadan kalkar. Üstüne: her kanal için benzersiz bir indeks ile <code>dedupe_key</code> — yeniden denemeler ikili teslimat yapamaz ve özel, bağlantı-yeterli ve meta IP'lere bir çıkış koruyucu ile engeller (ve yönlendirmeleri reddeder) — varış URL'leri operatör tarafından sağlanır, bu nedenle SSRF menümüzde mevcuttur.</p>
<h2><a name="render-as-text-not-html" href="#render-as-text-not-html"></a>Render as text, not HTML</h2>
<p>Temsilci yanıtlar HTML olarak saklanmıştır; widget bunları <code>textContent</code> ile işlemiştir, böylece kullanıcılar gerçek <code/> etiketlerini görmüştür. Cazip düzeltme, <code>innerHTML</code> kullanmaktır — üçüncü taraf bir sayfada, bu bir XSS riski taşır. Bunun yerine: pasif bir <code>DOMParser</code> belgesi içinde parçalayın (hiçbir script çalışmaz, hiçbir şey yüklenmez) ve metni çıkarın, blok yapısını koruyarak:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="kd">function</span> <span class="nf">htmlToText</span><span class="p">(</span><span class="nx">raw</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">doc</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DOMParser</span><span class="p">().</span><span class="nf">parseFromString</span><span class="p">(</span><span class="nc">String</span><span class="p">(</span><span class="nx">raw</span><span class="p">),</span> <span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">doc</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nf">querySelectorAll</span><span class="p">(</span><span class="dl">'</span><span class="s1">p, div, li, br</span><span class="dl">'</span><span class="p">).</span><span class="nf">forEach</span><span class="p">((</span><span class="nx">node</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">node</span><span class="p">[</span><span class="nx">node</span><span class="p">.</span><span class="nx">tagName</span> <span class="o">===</span> <span class="dl>'</span><span class="s1">BR</span><span class="dl>'</span> <span class="p">?</span> <span class="dl>'</span><span class="s1">before</span><span class="dl>'</span> <span class="p">:</span> <span class="dl>'</span><span class="s1">append</span><span class="dl>'</span><span class="p">](</span><span class="dl>'</span><span class="se">\n</span><span class="dl>'</span><span class="p">);</span>
<span class="p">});</span>
<span class="k">return</span> <span class="nx">doc</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">textContent</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">\n{3,}</span><span class="sr">/g</span><span class="p">,</span> <span class="dl>'</span><span class="se">\n\n</span><span class="dl>'</span><span class="p">).</span><span class="nf">trim</span><span class="p">();</span>
<span class="p">}</span>
</code></pre>
</div>
<h2><a name="test-the-paranoid-paths" href="#test-the-paranoid-paths"></a>Test the paranoid paths</h2>
<div class="highlight js-code-highlight">
<pre class="highlight php"><code><span class="nf">it</span><span class="p">(</span><span class="s1>'rejects identity fields containing the tuple delimiter'</span><span class="p">,</span> <span class="k">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="nf">widgetEntry</span><span class="p">([</span><span class="s1>'email'</span> <span class="o">=></span> <span class="s1>'victim|[email protected]'</span><span class="p">])</span><span class="o">-></span><span class="nf">assertForbidden</span><span class="p">();</span>
<span class="p">});</span>
<span class="nf">it</span><span class="p">(</span><span class="s1>'does not double-deliver a webhook for the same dedupe key'</span><span class="p">,</span> <span class="k">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="nv">$send</span> <span class="o">=</span> <span class="nf">app</span><span class="p">(</span><span class="nc">SendWebhook</span><span class="o">::</span><span class="n">class</span><span class="p">);</span>
<span class="nv">$send</span><span class="o">-></span><span class="nf">execute</span><span class="p">(</span><span class="nv">$channel</span><span class="p">,</span> <span class="s1>'ticket.replied'</span><span class="p">,</span> <span class="nv">$payload</span><span class="p">,</span> <span class="s2>"ticket.replied:</span><span class="si">{</span><span class="nv">$uuid</span><span class="si">}</span><span class="s2>"</span><span class="p">);</span>
<span class="nv">$send</span><span class="o">-></span><span class="nf">execute</span><span class="p">(</span><span class="nv">$channel</span><span class="p">,</span> <span class="s1>'ticket.replied'</span><span class="p">,</span> <span class="nv">$payload</span><span class="p">,</span> <span class="s2>"ticket.replied:</span><span class="si>{</span><span class="nv>$uuid</span><span class="si}>}</span><span class="s2>"</span><span class="p">);</span>
<span class="nf">expect</span><span class="p">(</span><span class="nc">WebhookDelivery</span><span class="o">::</span><span class="nb">count</span><span class="p">())</span><span class="o">-></span><span class="nf">toBe</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">});</span>
</code></pre>
</div>
<h2><a name="takeaway" href="#takeaway"></a>Takeaway</h2>
<p>Her dış yüzey üç iş yapar: kim içeri girebilir (kimlik doğrulama), ne yapabilecekleri (sınırlar ve kısıtlamalar) ve ne gönderiyorsunuz (imzalı, idempotent, çıkış korumalı). Lansmandan önce düşman gözden geçirmeyi ayarlayın — benimki dört bulgu ile kendini ödedi.</p>

