CVE-2021-32054: Use of Incorrectly-Resolved Name or Reference (CWE-706)
value: Firely/Incendi Spark before 1.5.5-r4 lacks Content-Disposition headers in certain situations, which may cause crafted files to be delivered to clients such that they are rendered directly in a victim's web browser.
CWE description: The product uses a name or reference to access a resource, but the name/reference resolves to a resource that is outside of the intended control sphere.
CVSS v3 base score: 6.1
Published: 2021-05-14T21:15Z
Repository: https://github.com/FirelyTeam/spark
Language: C#
Files changed: src/Spark.Engine/Core/HttpHeaderName.cs, src/Spark.Engine/Extensions/HttpRequestFhirExtensions.cs, src/Spark.Engine/Formatters/NetCore/BinaryOutputFormatter.cs, src/Spark.Engine/Handlers/NetCore/FormatTypeHandler.cs
Commit date: 2021-05-10 09:56:27 +0200
Commit message: refactor: Add constants for HTTP Header field names

Added constants for HTTP Header field names

Set Content-Disposition header to attachment for Binary requests

Diff: diff --git a/src/Spark.Engine/Core/HttpHeaderName.cs b/src/Spark.Engine/Core/HttpHeaderName.cs
new file mode 100644
index 0000000..2b9330e
--- /dev/null
+++ b/src/Spark.Engine/Core/HttpHeaderName.cs
@@ -0,0 +1,15 @@
+﻿namespace Spark.Engine.Core
+{
+    internal static class HttpHeaderName
+    {
+        public const string ACCEPT = "Accept";
+        public const string CONTENT_DISPOSITION = "Content-Disposition";
+        public const string CONTENT_LOCATION = "Content-Location";
+        public const string CONTENT_TYPE = "Content-Type";
+        public const string ETAG = "ETag";
+        public const string LOCATION = "Location";
+        public const string LAST_MODIFIED = "Last-Modified";
+
+        public const string X_CONTENT_TYPE = "X-Content-Type";
+    }
+}
diff --git a/src/Spark.Engine/Extensions/HttpRequestFhirExtensions.cs b/src/Spark.Engine/Extensions/HttpRequestFhirExtensions.cs
index ef5e371..7d8d9a7 100644
--- a/src/Spark.Engine/Extensions/HttpRequestFhirExtensions.cs
+++ b/src/Spark.Engine/Extensions/HttpRequestFhirExtensions.cs
@@ -156,21 +156,21 @@ namespace Spark.Engine.Extensions
 
         internal static void AcquireHeaders(this HttpResponse response, FhirResponse fhirResponse)
         {
             if (fhirResponse.Key != null)
             {
-                response.Headers.Add("ETag", ETag.Create(fhirResponse.Key.VersionId)?.ToString());
+                response.Headers.Add(HttpHeaderName.ETAG, ETag.Create(fhirResponse.Key.VersionId)?.ToString());
 
                 Uri location = fhirResponse.Key.ToUri();
-                response.Headers.Add("Location", location.OriginalString);
+                response.Headers.Add(HttpHeaderName.LOCATION, location.OriginalString);
 
                 if (response.Body != null)
                 {
-                    response.Headers.Add("Content-Location", location.OriginalString);
+                    response.Headers.Add(HttpHeaderName.CONTENT_LOCATION, location.OriginalString);
                     if (fhirResponse.Resource != null && fhirResponse.Resource.Meta != null)
                     {
-                        response.Headers.Add("Last-Modified", fhirResponse.Resource.Meta.LastUpdated.Value.ToString("R"));
+                        response.Headers.Add(HttpHeaderName.LAST_MODIFIED, fhirResponse.Resource.Meta.LastUpdated.Value.ToString("R"));
                     }
                 }
             }
         }
 
@@ -190,10 +190,14 @@ namespace Spark.Engine.Extensions
                     response.Content.Headers.ContentLocation = location;
                     if (fhirResponse.Resource != null && fhirResponse.Resource.Meta != null)
                     {
                         response.Content.Headers.LastModified = fhirResponse.Resource.Meta.LastUpdated;
                     }
+                    if(fhirResponse.Resource is Binary)
+                    {
+                        response.Content.Headers.Add(HttpHeaderName.CONTENT_DISPOSITION, "attachment");
+                    }
                 }
             }
         }
 
         private static HttpResponseMessage CreateBareFhirResponse(this HttpRequestMessage request, FhirResponse fhir)
diff --git a/src/Spark.Engine/Formatters/NetCore/BinaryOutputFormatter.cs b/src/Spark.Engine/Formatters/NetCore/BinaryOutputFormatter.cs
index f825d15..f770a38 100644
--- a/src/Spark.Engine/Formatters/NetCore/BinaryOutputFormatter.cs
+++ b/src/Spark.Engine/Formatters/NetCore/BinaryOutputFormatter.cs
@@ -35,10 +35,11 @@ namespace Spark.Engine.Formatters
 
                     binary = response.Resource as FhirModel.Binary;
                 }
                 if (binary == null) return;
 
+                context.HttpContext.Response.Headers.Add(HttpHeaderName.CONTENT_DISPOSITION, "attachment");
                 context.HttpContext.Response.ContentType = binary.ContentType;
 
                 Stream stream = new MemoryStream(binary.Data);
                 await stream.CopyToAsync(context.HttpContext.Response.Body);
             }
diff --git a/src/Spark.Engine/Handlers/NetCore/FormatTypeHandler.cs b/src/Spark.Engine/Handlers/NetCore/FormatTypeHandler.cs
index cadce94..248a2c7 100644
--- a/src/Spark.Engine/Handlers/NetCore/FormatTypeHandler.cs
+++ b/src/Spark.Engine/Handlers/NetCore/FormatTypeHandler.cs
@@ -23,24 +23,24 @@ namespace Spark.Engine.Handlers.NetCore
             if (!string.IsNullOrEmpty(format))
             {
                 ResourceFormat accepted = ContentType.GetResourceFormatFromFormatParam(format);
                 if (accepted != ResourceFormat.Unknown)
                 {
-                    if (context.Request.Headers.ContainsKey("Accept")) context.Request.Headers.Remove("Accept");
+                    if (context.Request.Headers.ContainsKey(HttpHeaderName.ACCEPT)) context.Request.Headers.Remove(HttpHeaderName.ACCEPT);
                     if (accepted == ResourceFormat.Json)
-                        context.Request.Headers.Add("Accept", new StringValues(ContentType.JSON_CONTENT_HEADER));
+                        context.Request.Headers.Add(HttpHeaderName.ACCEPT, new StringValues(ContentType.JSON_CONTENT_HEADER));
                     else
-                        context.Request.Headers.Add("Accept", new StringValues(ContentType.XML_CONTENT_HEADER));
+                        context.Request.Headers.Add(HttpHeaderName.ACCEPT, new StringValues(ContentType.XML_CONTENT_HEADER));
                 }
             }
 
             if (context.Request.IsRawBinaryPostOrPutRequest())
             {
                 if (!HttpRequestExtensions.IsContentTypeHeaderFhirMediaType(context.Request.ContentType))
                 {
                     string contentType = context.Request.ContentType;
-                    context.Request.Headers.Add("X-Content-Type", contentType);
+                    context.Request.Headers.Add(HttpHeaderName.X_CONTENT_TYPE, contentType);
                     context.Request.ContentType = FhirMediaType.OctetStreamMimeType;
                 }
             }
 
             await _next(context);

Vulnerable code:                 response.Headers.Add("ETag", ETag.Create(fhirResponse.Key.VersionId)?.ToString());
                response.Headers.Add("Location", location.OriginalString);
                    response.Headers.Add("Content-Location", location.OriginalString);
                        response.Headers.Add("Last-Modified", fhirResponse.Resource.Meta.LastUpdated.Value.ToString("R"));
                    if (context.Request.Headers.ContainsKey("Accept")) context.Request.Headers.Remove("Accept");
                        context.Request.Headers.Add("Accept", new StringValues(ContentType.JSON_CONTENT_HEADER));
                        context.Request.Headers.Add("Accept", new StringValues(ContentType.XML_CONTENT_HEADER));
                    context.Request.Headers.Add("X-Content-Type", contentType);
Fixed code: ﻿namespace Spark.Engine.Core
{
    internal static class HttpHeaderName
    {
        public const string ACCEPT = "Accept";
        public const string CONTENT_DISPOSITION = "Content-Disposition";
        public const string CONTENT_LOCATION = "Content-Location";
        public const string CONTENT_TYPE = "Content-Type";
        public const string ETAG = "ETag";
        public const string LOCATION = "Location";
        public const string LAST_MODIFIED = "Last-Modified";

        public const string X_CONTENT_TYPE = "X-Content-Type";
    }
}
                response.Headers.Add(HttpHeaderName.ETAG, ETag.Create(fhirResponse.Key.VersionId)?.ToString());
                response.Headers.Add(HttpHeaderName.LOCATION, location.OriginalString);
                    response.Headers.Add(HttpHeaderName.CONTENT_LOCATION, location.OriginalString);
                        response.Headers.Add(HttpHeaderName.LAST_MODIFIED, fhirResponse.Resource.Meta.LastUpdated.Value.ToString("R"));
                    if(fhirResponse.Resource is Binary)
                    {
                        response.Content.Headers.Add(HttpHeaderName.CONTENT_DISPOSITION, "attachment");
                    }
                context.HttpContext.Response.Headers.Add(HttpHeaderName.CONTENT_DISPOSITION, "attachment");
                    if (context.Request.Headers.ContainsKey(HttpHeaderName.ACCEPT)) context.Request.Headers.Remove(HttpHeaderName.ACCEPT);
                        context.Request.Headers.Add(HttpHeaderName.ACCEPT, new StringValues(ContentType.JSON_CONTENT_HEADER));
                        context.Request.Headers.Add(HttpHeaderName.ACCEPT, new StringValues(ContentType.XML_CONTENT_HEADER));
                    context.Request.Headers.Add(HttpHeaderName.X_CONTENT_TYPE, contentType);