This document describes the security hardening applied to the ElBruno.Text2Image library and the reasoning behind each measure. If you are contributing to or extending this library, please follow these patterns.
Risk: Under concurrent access, multiple threads could simultaneously create StableDiffusionPipeline instances, leaking unmanaged ONNX Runtime sessions and causing resource exhaustion.
Mitigation: All four local generators (StableDiffusion15, StableDiffusion21, SdxlTurbo, LcmDreamshaperV7) use the double-check locking pattern to ensure the pipeline is only created once, even under concurrent calls:
private readonly object _pipelineLock = new();
private StableDiffusionPipeline? _pipeline;
// Inside GenerateAsync:
if (_pipeline == null)
{
lock (_pipelineLock)
{
if (_pipeline == null)
{
var sessionOptions = SessionOptionsHelper.Create(options.ExecutionProvider);
_pipeline = new StableDiffusionPipeline(modelPath, sessionOptions, EmbeddingDim);
}
}
}Why not Lazy<T>? The pipeline creation depends on runtime options (execution provider, model path) that come from the caller, so a simple Lazy<T> with a fixed factory is not sufficient.
Risk: The FLUX.2 cloud API may return an image URL in its response. If the library blindly follows that URL with the API key attached, a malicious or compromised server could redirect to an attacker-controlled endpoint and capture the credential (Server-Side Request Forgery / credential leakage).
Mitigation: When downloading from a server-returned URL, a separate HttpRequestMessage is used without the api-key header:
// Use a separate request WITHOUT the API key to avoid credential leakage (SSRF mitigation)
using var imageRequest = new HttpRequestMessage(HttpMethod.Get, imageData.Url);
var imageResponse = await _httpClient.SendAsync(imageRequest, cancellationToken);The API key is only ever sent to the configured endpoint, never to URLs returned in API responses.
Risk: Adding the API key to HttpClient.DefaultRequestHeaders mutates shared state. If a user provides a shared or pooled HttpClient (e.g., from IHttpClientFactory), the key would leak to every other request made by that client in the application.
Mitigation: The API key is attached per-request via HttpRequestMessage.Headers, not via DefaultRequestHeaders:
using var request = new HttpRequestMessage(HttpMethod.Post, _endpoint);
request.Headers.TryAddWithoutValidation("api-key", _apiKey);
request.Content = JsonContent.Create(requestBody, Flux2JsonContext.Default.Flux2Request);
var response = await _httpClient.SendAsync(request, cancellationToken);This is safe for shared, pooled, or singleton HttpClient instances.
Risk: When the FLUX.2 API returns an error, the full response body could contain internal server details, stack traces, or other sensitive information. Propagating unbounded error text in exceptions could expose this data to logs, monitoring, or end users.
Mitigation: Error response bodies are truncated to a maximum of 1,024 characters before being included in exception messages:
private const int MaxErrorBodyLength = 1024;
var errorBody = await response.Content.ReadAsStringAsync(cancellationToken);
if (errorBody.Length > MaxErrorBodyLength)
errorBody = errorBody[..MaxErrorBodyLength] + "... (truncated)";
throw new HttpRequestException(
$"FLUX.2 API returned {response.StatusCode}: {errorBody}");Risk: Accepting arbitrary width/height values could lead to:
- Denial of service via unbounded memory allocation (e.g.,
Width = int.MaxValue) - ONNX Runtime crashes from dimensions that aren't multiples of 8 (required by the model architecture)
Mitigation: The ImageGenerationOptions.Width and ImageGenerationOptions.Height properties enforce strict validation in their setters:
public int Width
{
get => _width;
set
{
ArgumentOutOfRangeException.ThrowIfLessThan(value, 64);
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 4096);
if (value % 8 != 0)
throw new ArgumentException("Width must be a multiple of 8.", nameof(value));
_width = value;
}
}| Constraint | Value | Reason |
|---|---|---|
| Minimum | 64 px | Smallest dimension that produces meaningful results |
| Maximum | 4,096 px | Prevents excessive memory allocation (~4K is max for most SD models) |
| Multiple of 8 | Required | ONNX model tensor dimensions require this alignment |
Risk: StableDiffusionPipeline creates two SessionOptions objects (one for GPU/preferred provider, one CPU fallback for the tokenizer). If these are not disposed, ONNX Runtime native memory leaks over time.
Mitigation: Both SessionOptions instances are stored as fields and disposed in the Dispose() method:
internal sealed class StableDiffusionPipeline : IDisposable
{
private readonly SessionOptions _sessionOptions;
private readonly SessionOptions _cpuOptions;
// ...
public void Dispose()
{
_textEncoder.Dispose();
_unet.Dispose();
_vaeDecoder.Dispose();
_sessionOptions.Dispose();
_cpuOptions.Dispose();
}
}All generators that own a pipeline also implement IDisposable and call _pipeline?.Dispose().
- Never add credentials to
DefaultRequestHeaders— always use per-requestHttpRequestMessage.Headers. - Never follow server-returned URLs with credentials — treat redirected URLs as untrusted.
- Always validate user-provided dimensions — enforce bounds and alignment constraints early.
- Always dispose ONNX sessions — track and dispose all
SessionOptionsandInferenceSessionobjects. - Use double-check locking for lazy-initialized shared resources (pipeline, sessions).
- Truncate external data before including in exception messages or logs.
- Use
CancellationTokenconsistently — all async methods accept and pass cancellation tokens.
If you discover a security vulnerability in this library, please report it responsibly by emailing the repository owner rather than opening a public issue. See the GitHub Security Policy for guidance.