HMAC ile İmzalanmış Medya URL'leri: Auth Header'ı Olmadan Dosya Erişimini Güvenceye Almak
İşte gerçekten çözmeye çalışana kadar basit görünen bir problem: korumalı dosyaları (PDF'ler, görseller, dokümanlar) bir tarayıcıya, tarayıcı bir Authorization header'ı gönderemiyorken nasıl sunarsınız? Düşünün — bir <img> etiketi, bir PDF yükleyen <iframe> ya da doğrudan bir dosya indirme bağlantısı. Bunların hiçbiri isteğe bir Bearer token ekleyemez. Ama yine de kullanıcının dosyaya erişim yetkisi olduğunu doğrulamanız gerekir.
DigiFlow'da bunu HMAC ile imzalanmış URL'lerle çözdüm ve bu, tüm uygulamadaki en temiz güvenlik desenlerinden biri oldu.
Problem, Ayrıntılı Olarak
DigiFlow binlerce kamu dokümanını işler. Bir kullanıcı bir doküman detay sayfasını görüntülediğinde, gömülü görseller, PDF önizlemeleri ve indirme bağlantıları görür. Naif yaklaşım, tüm medyayı herkese açık hale getirmektir, ancak bu kamu dokümanları için kesinlikle olmaz.
İkinci yaklaşım, her şeyi auth header'larını kontrol eden bir API endpoint'i üzerinden proxy'lemektir:
html<!-- Bu işe yaramaz - img etiketleri Authorization header'ı gönderemez -->
<img src="/api/media/123" />
Bunu, fetch ile oluşturulan blob URL'leriyle aşabilirsiniz, ama bu, her görselin bir JavaScript fetch çağrısı, blob oluşturma ve URL iptali gerektirmesi demektir. 20 görselli bir sayfa için bu, berbat bir kullanıcı deneyimidir.
HMAC Çözümü
Fikir basit: sunucunun, bu belirli kullanıcıya bu belirli dosyaya sınırlı bir süre için erişim yetkisi verdiğini kanıtlayan kriptografik bir imza içeren bir URL üretmek. Auth header'ı gerekmez — yetkilendirme URL'nin kendisine gömülüdür.
Sunucu Tarafı: İmzalı URL'ler Üretmek
csharppublic class MediaUrlService : IMediaUrlService
{
private readonly byte[] _signingKey;
private readonly ICurrentUserService _currentUser;
private readonly IOptions<MediaOptions> _options;
public string GenerateSignedUrl(int mediaId, MediaAccessLevel accessLevel)
{
var expiry = DateTimeOffset.UtcNow
.Add(_options.Value.UrlExpiry) // Varsayılan: 15 dakika
.ToUnixTimeSeconds();
var userId = _currentUser.UserId;
// Yük: mediaId + userId + expiry + accessLevel
var payload = $"{mediaId}:{userId}:{expiry}:{accessLevel}";
// HMAC-SHA256 imzası
using var hmac = new HMACSHA256(_signingKey);
var signatureBytes = hmac.ComputeHash(
Encoding.UTF8.GetBytes(payload));
var signature = Convert.ToBase64String(signatureBytes)
.Replace("+", "-")
.Replace("/", "_")
.TrimEnd('='); // URL-güvenli Base64
return $"/api/media/{mediaId}" +
$"?uid={userId}" +
$"&exp={expiry}" +
$"&lvl={(int)accessLevel}" +
$"&sig={signature}";
}
}
İmzalı URL, doğrulama için gereken tüm bağlamı içerir: kimin eriştiği, ne zaman süresinin dolacağı ve hangi erişim seviyesinin verildiği. HMAC imzası, bu değerlerin hiçbirinin kurcalanamayacağını garanti eder.
Sunucu Tarafı: İmzalı URL'leri Doğrulamak
csharp[ApiController]
[Route("api/[controller]")]
[AllowAnonymous] // Auth header'ı gerekmez - imza, yetkilendirmenin kendisidir
public class MediaController : ControllerBase
{
private readonly byte[] _signingKey;
private readonly IMediaRepository _mediaRepo;
[HttpGet("{id}")]
public async Task<IActionResult> Get(
int id,
[FromQuery] int uid,
[FromQuery] long exp,
[FromQuery] int lvl,
[FromQuery] string sig)
{
// 1. Süre dolumunu kontrol et
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (exp < now)
return StatusCode(410, new { error = "URL expired" });
// 2. İmzayı yeniden oluştur ve doğrula
var payload = $"{id}:{uid}:{exp}:{lvl}";
using var hmac = new HMACSHA256(_signingKey);
var expectedSig = Convert.ToBase64String(
hmac.ComputeHash(Encoding.UTF8.GetBytes(payload)))
.Replace("+", "-")
.Replace("/", "_")
.TrimEnd('=');
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(sig),
Encoding.UTF8.GetBytes(expectedSig)))
{
return Forbid();
}
// 3. Dosyayı sun
var media = await _mediaRepo.GetByIdAsync(id);
if (media is null) return NotFound();
// 4. Erişim seviyesini kontrol et
var accessLevel = (MediaAccessLevel)lvl;
if (accessLevel == MediaAccessLevel.Preview && media.Size > 10_000_000)
return BadRequest("File too large for preview");
return File(media.Content, media.ContentType, media.FileName);
}
}
Vurgulanmaya değer iki güvenlik ayrıntısı var:
-
CryptographicOperations.FixedTimeEqualszamanlama saldırılarını (timing attacks) önler. Naif bir string karşılaştırması, kaç karakterin eşleştiği hakkında bilgi sızdırır ve bir saldırganın imzayı bayt bayt tahmin etmesine olanak tanır. Sabit zamanlı karşılaştırma, uyuşmazlığın nerede oluştuğundan bağımsız olarak her zaman aynı süreyi alır. -
Endpoint
[AllowAnonymous]'tur, çünkü imzanın KENDİSİ yetkilendirmedir. Standart auth middleware'i atlanır ve yerini HMAC doğrulaması alır.
Frontend: İmzalı URL'leri Kullanmak
React tarafında, API, imzalı URL'leri doküman yanıtının bir parçası olarak döndürür:
typescriptinterface DocumentDetail {
id: number;
title: string;
attachments: Array<{
id: number;
fileName: string;
contentType: string;
previewUrl: string; // HMAC imzalı, 15 dk'da süresi dolar
downloadUrl: string; // HMAC imzalı, 15 dk'da süresi dolar
}>;
}
function DocumentAttachments({ attachments }: Props) {
return (
<div>
{attachments.map((att) => (
<div key={att.id}>
{att.contentType.startsWith('image/') ? (
// İmzalı URL img etiketlerinde doğrudan çalışır
<img src={att.previewUrl} alt={att.fileName} />
) : att.contentType === 'application/pdf' ? (
// İmzalı URL iframe'lerde çalışır
<iframe src={att.previewUrl} title={att.fileName} />
) : (
// İmzalı URL indirme bağlantısı olarak çalışır
<a href={att.downloadUrl} download>{att.fileName}</a>
)}
</div>
))}
</div>
);
}
Fetch çağrısı yok, blob URL'leri yok, karmaşıklık yok. İmzalı URL'ler, sıradan bir URL'nin çalıştığı her yerde çalışır.
URL Yenileme
URL'lerin süresi 15 dakika sonra dolar. Uzun ömürlü sayfalar için arka planda yenileme, URL'lerin bayatlamamasını sağlar:
typescriptfunction useRefreshableMediaUrls(documentId: number) {
return useQuery({
queryKey: ['document-media', documentId],
queryFn: () => documentService.getMediaUrls(documentId),
refetchInterval: 12 * 60 * 1000, // Her 12 dakikada bir yenile
// URL'lerin süresi 15 dk'da dolar, 12 dk'da yenileme 3 dk tampon sağlar
});
}
Güvenlik Değerlendirmeleri
Anahtar Rotasyonu
HMAC imzalama anahtarı periyodik olarak rotasyona tabi tutulmalıdır. Rotasyon sırasında, aktif URL'leri bozmamak için hem eski hem de yeni anahtarlar doğrulama için kabul edilmelidir:
csharppublic bool ValidateSignature(string payload, string signature)
{
// Önce mevcut anahtarı dene
if (Validate(payload, signature, _currentKey)) return true;
// Önceki anahtara düş (rotasyondan önce üretilen URL'ler için)
if (_previousKey is not null && Validate(payload, signature, _previousKey))
return true;
return false;
}
Kurcalanamayan Şeyler
HMAC imzası kullanıcı kimliğini, süre dolumunu ve erişim seviyesini kapsadığı için:
- Bir kullanıcı URL'nin süresini uzatamaz
- Bir kullanıcı başkasının kimliğine bürünmek için kullanıcı kimliğini değiştiremez
- Bir kullanıcı önizleme erişiminden indirme erişimine yükseltme (escalate) yapamaz
- Bir kullanıcı farklı bir dosyaya erişmek için medya kimliğini değiştiremez
Herhangi bir parametrede yapılan herhangi bir değişiklik, imzayı geçersiz kılar.
HMAC İmzalı URL'lerin Korumadığı Şeyler
Sınırlamalar konusunda dürüst olun:
- URL paylaşımı: Bir kullanıcı imzalı URL'yi paylaşırsa, süresi dolana kadar herkes ona erişebilir. Kısa süre dolumu (15 dakika) bunu hafifletir.
- Referer sızıntısı: İmzalı URL, tarayıcının adres çubuğunda ve ağ sekmesinde görünür. Üçüncü taraf sitelere sızmasını önlemek için
Referrer-Policy: no-referrerheader'larını kullanın. - Sunucu tarafı anahtar ele geçirilmesi: İmzalama anahtarı sızdırılırsa, tüm URL'ler taklit edilebilir. Anahtarı Azure Key Vault ya da benzeri bir yerde saklayın.
Temel Çıkarımlar
- HMAC imzalı URL'ler, "HTML öğelerinde auth header'ı yok" problemini temiz ve güvenli şekilde çözer.
- Zamanlama saldırılarını önlemek için imza doğrulamasında sabit zamanlı karşılaştırma kullanın.
- URL'lerin devredilemez olması için yüke (payload) kullanıcı kimliğini dahil edin.
- Süre dolumunu kısa tutun (15 dakika) ve uzun ömürlü sayfalarda proaktif olarak yenileyin.
- Daha en baştan anahtar rotasyonu için plan yapın — rotasyon sırasında iki anahtarı aynı anda kabul edin.