<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><language>en</language><title>Blog posts by Cuong Nguyen Đinh</title> <link>https://world.optimizely.com/blogs/cuong-nguyen-inh/</link><description></description><ttl>60</ttl><generator>Optimizely World</generator><item> <title>Handling SixLabors.ImageSharp vulnerability warnings in Optimizely CMS 12 (unofficial)</title>            <link>https://world.optimizely.com/blogs/cuong-nguyen-inh/dates/2026/10/handling-sixlabors.imagesharp-vulnerability-warnings-in-optimizely-cms-12-unofficial</link>            <description>&lt;p&gt;&lt;strong&gt;UPDATED: Optimizely is officially working with SixLabors to find a solution, if possible; the work ID is CMS-57231. An official decision/announcement might come soon.&lt;/strong&gt;&lt;em&gt;&lt;br /&gt;&lt;br /&gt;Written by Claude Opus 5.5 (Anthropic) on behalf of Cuong Nguyen, Optimizely Technical Support.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A note from Cuong:&lt;/strong&gt; Thanks to Optimizely for giving me access to Claude, which made this kind of deep dive possible within a support ticket.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SixLabors.ImageSharp&lt;/strong&gt; is one of the most popular image libraries in the .NET world. Chances are it&#39;s in your solution too, even if you never added it yourself. Then one morning you open the project, run a build like any other day, and the output is suddenly full of &lt;strong&gt;NU1902&lt;/strong&gt;/&lt;strong&gt;NU1903&lt;/strong&gt; warnings. Nothing changed in your code. If &lt;strong&gt;TreatWarningsAsErrors&lt;/strong&gt; is on, the build simply fails.&lt;/p&gt;
&lt;p&gt;That&#39;s the story behind a lot of tickets we&#39;ve been getting in Optimizely Support lately. This post covers where the dependency comes from, why &quot;just upgrade ImageSharp&quot; doesn&#39;t work, and the four options you have today, including a SkiaSharp-based &lt;strong&gt;IImageService&lt;/strong&gt; sample.&lt;/p&gt;
&lt;h2&gt;1. What you&#39;ll see&lt;/h2&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code&gt;$ dotnet list package --include-transitive --vulnerable

   Transitive Package          Resolved   Severity   Advisory URL
   &amp;gt; SixLabors.ImageSharp      3.1.11     Moderate   https://github.com/advisories/GHSA-gwg2-r3hj-4w44
                                          High       https://github.com/advisories/GHSA-j3p4-wp97-rph4
                                          High       https://github.com/advisories/GHSA-j9gm-c75j-xc9q
                                          Moderate   https://github.com/advisories/GHSA-wmxv-xphr-5c9g
                                          High       https://github.com/advisories/GHSA-jjfr-hcj7-qf5w
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&#39;ll also get &lt;strong&gt;NU1902&lt;/strong&gt;/&lt;strong&gt;NU1903&lt;/strong&gt; during restore and build. With &lt;strong&gt;TreatWarningsAsErrors&lt;/strong&gt; enabled, the build fails.&lt;/p&gt;
&lt;h2&gt;2. Where the dependency comes from&lt;/h2&gt;
&lt;p&gt;The CMS uses &lt;strong&gt;IImageService&lt;/strong&gt; for thumbnails and the image editor. The default implementation lives in &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt;, which depends on &lt;strong&gt;SixLabors.ImageSharp&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code&gt;EPiServer.CMS 12.34.x
└── EPiServer.ImageLibrary.ImageSharp  (&amp;gt;= 1.0.6)   &amp;larr; NuGet picks the lowest matching version
    └── SixLabors.ImageSharp 2.1.11
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;NuGet resolves dependencies with the &lt;em&gt;lowest applicable version&lt;/em&gt; rule (&lt;a href=&quot;https://learn.microsoft.com/nuget/concepts/dependency-resolution#lowest-applicable-version&quot;&gt;docs&lt;/a&gt;). So &lt;strong&gt;EPiServer.CMS&lt;/strong&gt; 12.34.8 resolves &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; &lt;strong&gt;1.0.6&lt;/strong&gt;, which brings in ImageSharp &lt;strong&gt;2.1.11&lt;/strong&gt;. This is standard NuGet behavior, not something specific to Optimizely packages.&lt;/p&gt;
&lt;p&gt;To get a newer version, reference the CMS components directly instead of the &lt;strong&gt;EPiServer.CMS&lt;/strong&gt; metapackage, and pin &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; to 2.0.6:&lt;/p&gt;
&lt;pre class=&quot;language-xml&quot;&gt;&lt;code&gt;&amp;lt;PackageReference Include=&quot;EPiServer.CMS.UI&quot; Version=&quot;12.34.6&quot; /&amp;gt;
&amp;lt;PackageReference Include=&quot;EPiServer.Hosting&quot; Version=&quot;12.24.1&quot; /&amp;gt;
&amp;lt;!-- ... --&amp;gt;
&amp;lt;PackageReference Include=&quot;EPiServer.ImageLibrary.ImageSharp&quot; Version=&quot;2.0.6&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That moves you to ImageSharp 3.1.11, which clears the older advisories but not the five listed above.&lt;/p&gt;
&lt;h2&gt;3. Why not force a newer ImageSharp?&lt;/h2&gt;
&lt;p&gt;The advisories above are fixed in ImageSharp 4.x. Six Labors changed the licensing model in 4.x, and Release builds require a license key (&lt;a href=&quot;https://github.com/SixLabors/ImageSharp/issues/3205#issuecomment-6062121101&quot;&gt;discussion&lt;/a&gt;). Forcing 4.x through &lt;strong&gt;Directory.Packages.props&lt;/strong&gt; therefore gives you a build error unless you have a Six Labors license. The 3.1.x line, the last one usable without a license, still carries the advisories.&lt;/p&gt;
&lt;h2&gt;4. Your options&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Remove &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No code, warnings disappear&lt;/td&gt;
&lt;td&gt;No image processing: thumbnails and the image editor stop working&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Provide your own &lt;a href=&quot;/CsClassLibraries/cms/EPiServer.ImageLibrary.IImageService?version=12&quot;&gt;&lt;strong&gt;IImageService&lt;/strong&gt;&lt;/a&gt;, e.g. with &lt;a href=&quot;https://github.com/mono/SkiaSharp&quot;&gt;SkiaSharp&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Clean audit, features kept&lt;/td&gt;
&lt;td&gt;Custom code that your team owns and maintains&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Suppress the audit warnings&lt;/td&gt;
&lt;td&gt;Quick&lt;/td&gt;
&lt;td&gt;The vulnerable package stays in your solution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Wait for an updated &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No custom code&lt;/td&gt;
&lt;td&gt;No committed timeline today&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;On risk:&lt;/strong&gt; the image service only processes media that authenticated editors upload through the CMS. It doesn&#39;t handle arbitrary anonymous input. That lowers the exposure, and editors validating images on upload lowers it further. Whether that&#39;s acceptable is a decision for your own security policy and threat model.&lt;/p&gt;
&lt;h2&gt;5. Option 2: a custom &lt;strong&gt;IImageService&lt;/strong&gt; with SkiaSharp&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;IImageService&lt;/strong&gt; is a public extension point with a single method:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;public interface IImageService
{
    void RenderImage(Stream input, Stream output, RenderRequest request);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The default implementation maps each &lt;strong&gt;ImageEditorCommand&lt;/strong&gt; like this, and a replacement should do the same:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;ImageEditorCommand&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Default (ImageSharp)&lt;/th&gt;
&lt;th&gt;SkiaSharp equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Grayscale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Grayscale()&lt;/strong&gt; (BT.709)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;SKColorFilter.CreateColorMatrix&lt;/strong&gt; with BT.709 coefficients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Crop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Crop(rect)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DrawBitmap(src, -left, -top)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FlipX&lt;/strong&gt; / &lt;strong&gt;FlipY&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Flip(Horizontal/Vertical)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;canvas.Scale(-1, 1, cx, 0)&lt;/strong&gt; / &lt;strong&gt;Scale(1, -1, 0, cy)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resize&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ResizeMode.Stretch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DrawBitmap(src, destRect, Mitchell)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ResizeKeepScale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ResizeMode.BoxPad&lt;/strong&gt; + &lt;strong&gt;BackgroundColor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;fit the box, &lt;strong&gt;no upscaling&lt;/strong&gt;, center, &lt;strong&gt;Clear(bg)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rotate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Rotate(deg)&lt;/strong&gt;, canvas expands&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Translate&lt;/strong&gt; + &lt;strong&gt;RotateDegrees&lt;/strong&gt; on an expanded canvas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Packages&lt;/h3&gt;
&lt;p&gt;Remove &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; and add SkiaSharp. If you reference the &lt;strong&gt;EPiServer.CMS&lt;/strong&gt; metapackage, switch to individual component references first (see section 2). The metapackage brings &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; in transitively, so you can&#39;t remove it otherwise.&lt;/p&gt;
&lt;pre class=&quot;language-xml&quot;&gt;&lt;code&gt;&amp;lt;!-- remove: &amp;lt;PackageReference Include=&quot;EPiServer.ImageLibrary.ImageSharp&quot; Version=&quot;...&quot; /&amp;gt; --&amp;gt;
&amp;lt;PackageReference Include=&quot;SkiaSharp&quot; Version=&quot;4.153.1&quot; /&amp;gt;
&amp;lt;!-- Required on Linux, containers and DXP: --&amp;gt;
&amp;lt;PackageReference Include=&quot;SkiaSharp.NativeAssets.Linux.NoDependencies&quot; Version=&quot;4.153.1&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The service&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ResizeKeepScale&lt;/strong&gt; matters most, because thumbnail generation calls it with &lt;strong&gt;BackgroundColor = &quot;#00000000&quot;&lt;/strong&gt;. The full implementation:&lt;/p&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using EPiServer.ImageLibrary;
using SkiaSharp;

namespace YourSite.Business.Imaging;

/// &amp;lt;summary&amp;gt;
/// SkiaSharp-based replacement for EPiServer.ImageLibrary.ImageSharp&#39;s DefaultImageService,
/// which pulls in vulnerable SixLabors.ImageSharp versions. Mirrors the default behavior:
/// same commands, ResizeKeepScale = ImageSharp &quot;BoxPad&quot; + background color, ZoomFactor applied last.
/// &amp;lt;/summary&amp;gt;
/// &amp;lt;remarks&amp;gt;
/// Skia only encodes PNG, JPEG and WebP. GIF/BMP/TIFF output is not supported (NotSupportedException),
/// and animated GIF input is flattened to its first frame.
/// &amp;lt;/remarks&amp;gt;
public class SkiaImageService : IImageService
{
    private static readonly SKSamplingOptions ResizeSampling = new(SKCubicResampler.Mitchell);
    private static readonly SKSamplingOptions RotateSampling = new(SKFilterMode.Linear);
    private static readonly SKSamplingOptions CopySampling = new(SKFilterMode.Nearest);

    // ITU-R BT.709 luma, same coefficients as ImageSharp&#39;s Grayscale().
    private static readonly float[] GrayscaleMatrix =
    [
        0.2126f, 0.7152f, 0.0722f, 0, 0,
        0.2126f, 0.7152f, 0.0722f, 0, 0,
        0.2126f, 0.7152f, 0.0722f, 0, 0,
        0, 0, 0, 1, 0,
    ];

    public void RenderImage(Stream input, Stream output, RenderRequest request)
    {
        ArgumentNullException.ThrowIfNull(input);
        ArgumentNullException.ThrowIfNull(output);
        ArgumentNullException.ThrowIfNull(request);

        var image = Decode(input);
        try
        {
            foreach (var operation in request.Operations ?? [])
            {
                image = Replace(image, Apply(image, operation));
            }

            if (request.ZoomFactor != 1f)
            {
                var width = (int)Math.Floor(image.Width * request.ZoomFactor);
                var height = (int)Math.Floor(image.Height * request.ZoomFactor);
                image = Replace(image, Resize(image, width, height));
            }

            Encode(image, output, request);
        }
        finally
        {
            image.Dispose();
        }
    }

    private static SKBitmap Decode(Stream input)
    {
        using var codec = SKCodec.Create(input)
            ?? throw new NotSupportedException(&quot;The source image format is not supported.&quot;);

        var info = codec.Info
            .WithColorType(SKImageInfo.PlatformColorType)
            .WithAlphaType(SKAlphaType.Premul);

        return SKBitmap.Decode(codec, info)
            ?? throw new NotSupportedException(&quot;The source image could not be decoded.&quot;);
    }

    private static SKBitmap Replace(SKBitmap current, SKBitmap next)
    {
        current.Dispose();
        return next;
    }

    private static SKBitmap Apply(SKBitmap source, ImageOperation operation) =&amp;gt; operation.Command switch
    {
        ImageEditorCommand.Grayscale =&amp;gt; Grayscale(source),
        ImageEditorCommand.Crop =&amp;gt; Crop(source, operation),
        ImageEditorCommand.FlipX =&amp;gt; Draw(source.Width, source.Height, canvas =&amp;gt;
        {
            canvas.Scale(-1, 1, source.Width / 2f, 0);
            canvas.DrawBitmap(source, 0, 0, CopySampling);
        }),
        ImageEditorCommand.FlipY =&amp;gt; Draw(source.Width, source.Height, canvas =&amp;gt;
        {
            canvas.Scale(1, -1, 0, source.Height / 2f);
            canvas.DrawBitmap(source, 0, 0, CopySampling);
        }),
        ImageEditorCommand.Resize =&amp;gt; Resize(source, operation.Width, operation.Height),
        ImageEditorCommand.ResizeKeepScale =&amp;gt; ResizeKeepScale(source, operation),
        ImageEditorCommand.Rotate =&amp;gt; Rotate(source, operation.Angle),
        _ =&amp;gt; throw new InvalidOperationException($&quot;Unknown command {operation.Command}&quot;),
    };

    private static SKBitmap Draw(int width, int height, Action&amp;lt;SKCanvas&amp;gt; draw)
    {
        var bitmap = new SKBitmap(width, height, SKImageInfo.PlatformColorType, SKAlphaType.Premul);
        using var canvas = new SKCanvas(bitmap);
        canvas.Clear(SKColors.Transparent);
        draw(canvas);
        return bitmap;
    }

    private static SKBitmap Grayscale(SKBitmap source)
    {
        using var filter = SKColorFilter.CreateColorMatrix(GrayscaleMatrix);
        using var paint = new SKPaint { ColorFilter = filter };
        return Draw(source.Width, source.Height, canvas =&amp;gt; canvas.DrawBitmap(source, 0, 0, CopySampling, paint));
    }

    private static SKBitmap Crop(SKBitmap source, ImageOperation operation)
    {
        var area = SKRectI.Create(operation.Left, operation.Top, operation.Width, operation.Height);
        if (area.IsEmpty || !SKRectI.Create(source.Width, source.Height).Contains(area))
        {
            throw new ArgumentOutOfRangeException(nameof(operation), $&quot;Crop area {area} is outside the image bounds.&quot;);
        }

        return Draw(area.Width, area.Height, canvas =&amp;gt; canvas.DrawBitmap(source, -area.Left, -area.Top, CopySampling));
    }

    private static SKBitmap Resize(SKBitmap source, int width, int height)
    {
        var size = TargetSize(source, width, height);
        return Draw(size.Width, size.Height, canvas =&amp;gt;
            canvas.DrawBitmap(source, SKRect.Create(size.Width, size.Height), ResizeSampling));
    }

    /// &amp;lt;summary&amp;gt;ImageSharp BoxPad: fit inside the box without upscaling, center, fill the rest with the background color.&amp;lt;/summary&amp;gt;
    private static SKBitmap ResizeKeepScale(SKBitmap source, ImageOperation operation)
    {
        var box = TargetSize(source, operation.Width, operation.Height);
        var scale = Math.Min(1f, Math.Min(box.Width / (float)source.Width, box.Height / (float)source.Height));
        var width = Math.Max(1, (int)MathF.Round(source.Width * scale));
        var height = Math.Max(1, (int)MathF.Round(source.Height * scale));
        var destination = SKRect.Create((box.Width - width) / 2, (box.Height - height) / 2, width, height);
        var background = ParseColor(operation.BackgroundColor);

        return Draw(box.Width, box.Height, canvas =&amp;gt;
        {
            canvas.Clear(background);
            canvas.DrawBitmap(source, destination, ResizeSampling);
        });
    }

    private static SKBitmap Rotate(SKBitmap source, double degrees)
    {
        var radians = degrees * Math.PI / 180;
        var cos = Math.Abs(Math.Cos(radians));
        var sin = Math.Abs(Math.Sin(radians));
        // Round before Ceiling so right angles don&#39;t grow by a pixel from floating point noise (50.0000001 -&amp;gt; 51).
        var width = (int)Math.Ceiling(Math.Round(source.Width * cos + source.Height * sin, 3));
        var height = (int)Math.Ceiling(Math.Round(source.Width * sin + source.Height * cos, 3));

        return Draw(width, height, canvas =&amp;gt;
        {
            canvas.Translate(width / 2f, height / 2f);
            canvas.RotateDegrees((float)degrees);
            canvas.DrawBitmap(source, -source.Width / 2f, -source.Height / 2f, RotateSampling);
        });
    }

    /// &amp;lt;summary&amp;gt;Same as ImageSharp: a 0 dimension is derived from the other one, keeping the aspect ratio.&amp;lt;/summary&amp;gt;
    private static SKSizeI TargetSize(SKBitmap source, int width, int height)
    {
        if (width == 0 &amp;amp;&amp;amp; height &amp;gt; 0)
        {
            width = Math.Max(1, (int)MathF.Round(source.Width * height / (float)source.Height));
        }

        if (height == 0 &amp;amp;&amp;amp; width &amp;gt; 0)
        {
            height = Math.Max(1, (int)MathF.Round(source.Height * width / (float)source.Width));
        }

        if (width &amp;lt;= 0 || height &amp;lt;= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(width), $&quot;Invalid target size {width}x{height}.&quot;);
        }

        return new SKSizeI(width, height);
    }

    /// &amp;lt;summary&amp;gt;Parses ImageSharp-style hex colors (#RGB, #RGBA, #RRGGBB, #RRGGBBAA). Empty means white.&amp;lt;/summary&amp;gt;
    private static SKColor ParseColor(string value)
    {
        if (string.IsNullOrEmpty(value))
        {
            return SKColors.White;
        }

        var hex = value.TrimStart(&#39;#&#39;);
        // Skia expects the alpha first (#ARGB / #AARRGGBB).
        var argb = hex.Length switch
        {
            4 =&amp;gt; hex[3..] + hex[..3],
            8 =&amp;gt; hex[6..] + hex[..6],
            _ =&amp;gt; hex,
        };

        return SKColor.TryParse(argb, out var color)
            ? color
            : throw new ArgumentException($&quot;Invalid background color &#39;{value}&#39;.&quot;, nameof(value));
    }

    private static void Encode(SKBitmap image, Stream output, RenderRequest request)
    {
        using var pixmap = image.PeekPixels();
        var quality = Math.Clamp(request.Quality, 0, 100);

        var encoded = request.MimeType switch
        {
            &quot;image/png&quot; or &quot;image/x-icon&quot; =&amp;gt;
                pixmap.Encode(output, new SKPngEncoderOptions(SKPngEncoderFilterFlags.AllFilters, PngZLibLevel(request.Quality))),
            &quot;image/jpg&quot; or &quot;image/jpe&quot; or &quot;image/jpeg&quot; or &quot;image/pjpeg&quot; =&amp;gt;
                pixmap.Encode(output, new SKJpegEncoderOptions(quality)),
            &quot;image/webp&quot; =&amp;gt;
                pixmap.Encode(output, new SKWebpEncoderOptions(SKWebpEncoderCompression.Lossy, quality)),
            _ =&amp;gt; throw new NotSupportedException($&quot;Not supported image format [{request.MimeType}] when creating images.&quot;),
        };

        if (!encoded)
        {
            throw new InvalidOperationException($&quot;Failed to encode image as [{request.MimeType}].&quot;);
        }
    }

    /// &amp;lt;summary&amp;gt;Same quality -&amp;gt; compression mapping as the default service: 100 = level 0 ... &amp;amp;lt;20 = level 9.&amp;lt;/summary&amp;gt;
    private static int PngZLibLevel(int quality) =&amp;gt; quality &amp;lt; 0 ? 6 : Math.Clamp(10 - quality / 10, 0, 9);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two details that are easy to miss:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Channel order&lt;/strong&gt; (&lt;strong&gt;ParseColor&lt;/strong&gt;). ImageSharp parses &lt;strong&gt;#RRGGBBAA&lt;/strong&gt;, Skia parses &lt;strong&gt;#AARRGGBB&lt;/strong&gt;. Rotate the hex string before parsing, or &lt;strong&gt;#00FF0080&lt;/strong&gt; comes out as a different color.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;90&amp;deg; rotation&lt;/strong&gt; (&lt;strong&gt;Rotate&lt;/strong&gt;). &lt;strong&gt;cos(90&amp;deg;)&lt;/strong&gt; evaluates to &lt;strong&gt;6e-17&lt;/strong&gt;, not 0, so &lt;strong&gt;Math.Ceiling(50.00000000000001)&lt;/strong&gt; gives 51 and the output gains an extra pixel. Round before taking the ceiling.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Registration&lt;/h3&gt;
&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code&gt;using EPiServer.ImageLibrary;
using YourSite.Business.Imaging;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddCmsAspNetIdentity&amp;lt;ApplicationUser&amp;gt;()
            .AddCms();

        // Replaces EPiServer.ImageLibrary.ImageSharp for thumbnails and the CMS image editor.
        services.AddSingleton&amp;lt;IImageService, SkiaImageService&amp;gt;();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CMS only registers a fallback &lt;strong&gt;IImageService&lt;/strong&gt; when none exists (&lt;strong&gt;TryAddSingleton&lt;/strong&gt;), so your implementation takes precedence. Without any registration, that fallback throws &lt;strong&gt;InvalidOperationException&lt;/strong&gt; the first time it&#39;s resolved.&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code&gt;$ dotnet list package --include-transitive --vulnerable
The given project `Alloy` has no vulnerable packages given the current sources.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Known limitations&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Skia encodes PNG, JPEG and WebP only. GIF, BMP or TIFF output throws &lt;strong&gt;NotSupportedException&lt;/strong&gt;, so the image editor won&#39;t work for those formats. Thumbnails aren&#39;t affected, because the CMS always stores them as &lt;strong&gt;.png&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Skia can&#39;t decode TIFF, so TIFF uploads get no thumbnail. The upload itself still succeeds, because the CMS handles thumbnail errors.&lt;/li&gt;
&lt;li&gt;Animated GIFs are flattened to the first frame.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; this sample is not an officially supported Optimizely package. It&#39;s provided as guidance, and you own it once it&#39;s in your solution. Test it against your own media library before you deploy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;6. Option 3: suppress the warnings&lt;/h2&gt;
&lt;pre class=&quot;language-xml&quot;&gt;&lt;code&gt;&amp;lt;ItemGroup&amp;gt;
  &amp;lt;NuGetAuditSuppress Include=&quot;https://github.com/advisories/GHSA-gwg2-r3hj-4w44&quot; /&amp;gt;
  &amp;lt;NuGetAuditSuppress Include=&quot;https://github.com/advisories/GHSA-j3p4-wp97-rph4&quot; /&amp;gt;
  &amp;lt;NuGetAuditSuppress Include=&quot;https://github.com/advisories/GHSA-j9gm-c75j-xc9q&quot; /&amp;gt;
  &amp;lt;NuGetAuditSuppress Include=&quot;https://github.com/advisories/GHSA-wmxv-xphr-5c9g&quot; /&amp;gt;
  &amp;lt;NuGetAuditSuppress Include=&quot;https://github.com/advisories/GHSA-jjfr-hcj7-qf5w&quot; /&amp;gt;
&amp;lt;/ItemGroup&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Suppressing only hides the warning, the vulnerability is still there. Record the decision (reason, owner, review date) wherever your team tracks accepted risks.&lt;/p&gt;
&lt;h2&gt;7. Option 4: wait for an updated package&lt;/h2&gt;
&lt;p&gt;An updated &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; would remove the problem at the source. We can&#39;t share a timeline for that yet. If you go this route:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Watch the Optimizely release notes for a new &lt;strong&gt;EPiServer.ImageLibrary.ImageSharp&lt;/strong&gt; version, or check from the command line:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code&gt;dotnet list package --outdated --include-transitive | grep -i imagelibrary
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Combine it with option 3 in the meantime, and add a review date so the suppression gets removed once the update ships:&lt;/p&gt;
&lt;pre class=&quot;language-xml&quot;&gt;&lt;code&gt;&amp;lt;!-- TEMP: waiting for an EPiServer.ImageLibrary.ImageSharp update without the vulnerable ImageSharp.
     Review by: 2026-12-31. Owner: @team-cms --&amp;gt;
&amp;lt;NuGetAuditSuppress Include=&quot;https://github.com/advisories/GHSA-j3p4-wp97-rph4&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If you&#39;re on option 2, moving to the official package later means deleting your &lt;strong&gt;IImageService&lt;/strong&gt; implementation and its registration.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;SixLabors/ImageSharp, 4.x license discussion: &lt;a href=&quot;https://github.com/SixLabors/ImageSharp/issues/3205#issuecomment-6062121101&quot;&gt;https://github.com/SixLabors/ImageSharp/issues/3205#issuecomment-6062121101&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Optimizely &lt;strong&gt;IImageService&lt;/strong&gt; API: &lt;a href=&quot;/CsClassLibraries/cms/EPiServer.ImageLibrary.IImageService?version=12&quot;&gt;https://world.optimizely.com/CsClassLibraries/cms/EPiServer.ImageLibrary.IImageService?version=12&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;SkiaSharp: &lt;a href=&quot;https://github.com/mono/SkiaSharp&quot;&gt;https://github.com/mono/SkiaSharp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NuGet dependency resolution: &lt;a href=&quot;https://learn.microsoft.com/nuget/concepts/dependency-resolution&quot;&gt;https://learn.microsoft.com/nuget/concepts/dependency-resolution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;NuGet Audit: &lt;a href=&quot;https://learn.microsoft.com/nuget/concepts/auditing-packages&quot;&gt;https://learn.microsoft.com/nuget/concepts/auditing-packages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Advisories: &lt;a href=&quot;https://github.com/advisories/GHSA-gwg2-r3hj-4w44&quot;&gt;GHSA-gwg2-r3hj-4w44&lt;/a&gt;, &lt;a href=&quot;https://github.com/advisories/GHSA-j3p4-wp97-rph4&quot;&gt;GHSA-j3p4-wp97-rph4&lt;/a&gt;, &lt;a href=&quot;https://github.com/advisories/GHSA-j9gm-c75j-xc9q&quot;&gt;GHSA-j9gm-c75j-xc9q&lt;/a&gt;, &lt;a href=&quot;https://github.com/advisories/GHSA-wmxv-xphr-5c9g&quot;&gt;GHSA-wmxv-xphr-5c9g&lt;/a&gt;, &lt;a href=&quot;https://github.com/advisories/GHSA-jjfr-hcj7-qf5w&quot;&gt;GHSA-jjfr-hcj7-qf5w&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>            <guid>https://world.optimizely.com/blogs/cuong-nguyen-inh/dates/2026/10/handling-sixlabors.imagesharp-vulnerability-warnings-in-optimizely-cms-12-unofficial</guid>            <pubDate>Sat, 10 Oct 2026 06:01:41 GMT</pubDate>           <category>Blog post</category></item><item> <title>Optimizely Content Graph on mobile application</title>            <link>https://world.optimizely.com/blogs/cuong-nguyen-inh/dates/2025/1/optimizely-content-graph-on-mobile-application/</link>            <description>&lt;p&gt;CG everywhere! I pull schema from our default index https://cg.optimizely.com/app/graphiql?auth=eBrGunULiC5TziTCtiOLEmov2LijBf30obh0KmhcBlyTktGZ in our document to a schema.graphql file via the introspection query, write a query that gets product pages, use a package to generate graph client code and pull data.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;Source code available at &lt;a href=&quot;https://github.com/cuong0993/cms_saas_flutter_demo&quot;&gt;https://github.com/cuong0993/cms_saas_flutter_demo&lt;/a&gt;&lt;/p&gt;</description>            <guid>https://world.optimizely.com/blogs/cuong-nguyen-inh/dates/2025/1/optimizely-content-graph-on-mobile-application/</guid>            <pubDate>Mon, 20 Jan 2025 12:17:42 GMT</pubDate>           <category>Blog post</category></item></channel>
</rss>