Skip to content

Add SVG support for custom organization logos - #44748

Merged
nulmete merged 7 commits into
mainfrom
add-svg-support-custom-logos
May 6, 2026
Merged

Add SVG support for custom organization logos#44748
nulmete merged 7 commits into
mainfrom
add-svg-support-custom-logos

Conversation

@nulmete

@nulmete nulmete commented May 5, 2026

Copy link
Copy Markdown
Member

Related issue: Follow-up to #44390 (BE/FE) and #44550 (GitOps). Parent story #39016.

Summary

Accepts .svg for organization logo uploads in addition to PNG/JPEG/WebP, with strict server-side validation since SVGs can carry scripts.

Checklist for submitter

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

Testing

  • Added/updated automated tests

  • QA'd all new/changed functionality manually

Screen.Recording.2026-05-05.at.8.59.12.PM.mov

Summary by CodeRabbit

  • New Features

    • Organization logos now accept SVG in addition to PNG, JPEG, and WebP.
    • Stored SVG logos are re-validated when served.
  • Security

    • Server applies strict SVG sanitization to block scripts, unsafe elements, event handlers, and unsafe URL schemes.
    • SVG logo responses include headers to prevent content-type sniffing and restrict execution.
  • Tests

    • Added tests covering SVG detection, validation, allowed/rejected cases, and serving behavior.

Follow-up to #44390 (BE/FE) and #44550 (GitOps). Accepts .svg in
addition to PNG/JPEG/WebP. Server-side validation parses the XML and
rejects script/foreignObject/iframe/object/embed elements, on*
attributes, javascript:/data: URLs in href/src, and DOCTYPE/ENTITY
declarations (XXE, billion-laughs vectors).

Defense-in-depth: GET re-validates on read so a blob planted directly
in the object store is still rejected, sets X-Content-Type-Options:
nosniff for all logos, and a strict CSP for SVGs so a user pasting the
URL into the address bar can't trigger scripts.
@codecov

codecov Bot commented May 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.15730% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.67%. Comparing base (f2b2e23) to head (8dd7ff9).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
frontend/utilities/file/orgLogoFile.ts 20.00% 12 Missing ⚠️
server/fleet/org_logo.go 89.85% 5 Missing and 2 partials ⚠️
server/service/org_logo.go 20.00% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #44748      +/-   ##
==========================================
- Coverage   66.68%   66.67%   -0.01%     
==========================================
  Files        2664     2664              
  Lines      214605   214680      +75     
  Branches     9876     9881       +5     
==========================================
+ Hits       143106   143145      +39     
- Misses      58478    58500      +22     
- Partials    13021    13035      +14     
Flag Coverage Δ
backend 68.55% <85.13%> (-0.01%) ⬇️
frontend 54.24% <20.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread server/service/org_logo.go Fixed
@nulmete
nulmete marked this pull request as ready for review May 5, 2026 16:11
@nulmete
nulmete requested review from a team as code owners May 5, 2026 16:12
Copilot AI review requested due to automatic review settings May 5, 2026 16:12

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

Addresses CodeQL "Incomplete URL scheme check" on #44748: a blocklist
of script-bearing schemes (javascript:, data:) misses vbscript:, file:,
livescript:, mocha:, and any future scheme. Allow only fragment,
relative, or http(s):// and reject everything else.
@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds SVG support for custom organization logos. Frontend: accepts .svg, expands allowed types, increases sniff window to 1024 bytes, and detects SVG by scanning the head for an element. Backend: detects image/svg+xml, re-validates stored logo bytes using a new SVG sanitizer that enforces an root, rejects disallowed elements (e.g., script, foreignObject, iframe), blocks on* attributes, restricts href/src/xml:base to fragments/relative or http(s), and disallows DOCTYPE/DTD/processing instructions. GET logo responses set X-Content-Type-Options: nosniff and a restrictive CSP for SVG. Tests added for detection and validation.

Possibly related PRs

  • fleetdm/fleet#44550: Extends org-logo validation and content-type detection to support and strictly sanitize SVGs, with server-side validation and tests.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR description includes related issue references, summary, changes file confirmation, testing notes, and a manual QA video. However, several required checklist items are incomplete or unchecked. Verify completion of security validation checklist (input validation, SQL injection, JS injection prevention) and database migration checklist items that may apply. Clarify which items are intentionally not applicable versus incomplete.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding SVG support for custom organization logos.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch add-svg-support-custom-logos

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends Fleet’s custom organization logo feature to accept SVG uploads in addition to raster formats, with server-side SVG validation and additional hardening headers when serving SVG content. It fits into the existing org logo upload/serve flow by adding SVG sniffing, XML-based sanitization, and tightening response headers for direct SVG access.

Changes:

  • Add SVG detection and strict XML token-based validation for org logo uploads (rejecting common script/XXE vectors).
  • Re-validate stored org logo bytes on read and harden logo responses with X-Content-Type-Options: nosniff and an SVG-specific CSP.
  • Update frontend logo file validation/accept list to include .svg, and add backend test coverage plus a real-world SVG fixture.

Reviewed changes

Copilot reviewed 3 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
server/service/testdata/icons/org_logo_css.svg Adds a real-world SVG fixture used to validate the server-side SVG parser behavior.
server/service/org_logo.go Implements SVG sniffing, SVG XML sanitization, GET hardening headers, and re-validation on read.
server/service/org_logo_test.go Adds unit tests covering accepted/rejected SVG cases and SVG content-type detection.
frontend/utilities/file/orgLogoFile.ts Allows selecting .svg and updates client-side sniffing to recognize SVG for UX validation.
changes/add-svg-support-custom-logos Adds release note entry for SVG logo support and sanitization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
server/service/org_logo.go (1)

486-490: 💤 Low value

Re-validation on every GET — note for hot paths.

Defense-in-depth re-validation here is reasonable given the threat model (objects planted directly in the bucket). Worth being aware that combined with Cache-Control: no-store (line 180) every logo fetch round-trips through XML parsing. For org logos this is fine, but if this handler is later reused for higher-traffic asset paths, consider validating once on retrieval and caching the validated bytes (or at least the validation result keyed by content hash).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/service/org_logo.go` around lines 486 - 490, The current re-validation
in validateOrgLogoBytes on every GET (called from the org logo read path) causes
repeated XML parsing on hot paths because responses are served with
Cache-Control: no-store; to fix, add a short-lived in-memory cache keyed by the
object's immutable identifier (e.g., ETag or content hash) and store either the
validated bytes or a validation-success flag so subsequent reads skip
validateOrgLogoBytes when the cache entry exists; ensure the cache is consulted
in the org logo retrieval flow before calling validateOrgLogoBytes and that
entries are evicted/invalidated when the object ETag changes or after a
configurable TTL to preserve defense-in-depth.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/service/org_logo.go`:
- Around line 263-278: The isSafeSVGURL function currently treats URLs with
empty Scheme as safe, which lets protocol-relative URLs (e.g. "//evil.com/x")
slip through because url.Parse sets Scheme=="" but Host is populated; update
isSafeSVGURL to reject authority-bearing URLs with an empty scheme by returning
false when u.Scheme=="" && u.Host!="" (i.e., treat network-relative URLs as
unsafe), keep allowing relative paths/fragments (empty scheme and empty host),
and retain explicit allow for "http"/"https" schemes; also add the test case
{"protocol-relative", "//evil.com/x.png", false} to the existing tests to cover
this scenario.
- Around line 280-344: Update the SVG validator to block SMIL animation elements
by adding "set", "animate", "animateTransform", and "animateMotion" to the
disallowedSVGElements map and ensure validateSVG will reject any occurrence of
those tags; then add a unit test in TestValidateOrgLogoBytesSVG that includes an
SVG using a <set> or <animate> element (e.g., <a href="#safe"><set
attributeName="href" to="javascript:..."/></a>) to assert the validator rejects
it to prevent regression.

---

Nitpick comments:
In `@server/service/org_logo.go`:
- Around line 486-490: The current re-validation in validateOrgLogoBytes on
every GET (called from the org logo read path) causes repeated XML parsing on
hot paths because responses are served with Cache-Control: no-store; to fix, add
a short-lived in-memory cache keyed by the object's immutable identifier (e.g.,
ETag or content hash) and store either the validated bytes or a
validation-success flag so subsequent reads skip validateOrgLogoBytes when the
cache entry exists; ensure the cache is consulted in the org logo retrieval flow
before calling validateOrgLogoBytes and that entries are evicted/invalidated
when the object ETag changes or after a configurable TTL to preserve
defense-in-depth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2459095e-1f2e-4ef1-b4da-e793b7196458

📥 Commits

Reviewing files that changed from the base of the PR and between dd9f924 and 5092846.

📒 Files selected for processing (2)
  • server/service/org_logo.go
  • server/service/org_logo_test.go

Comment thread server/service/org_logo.go Outdated
Comment on lines +263 to +278
func isSafeSVGURL(raw string) bool {
raw = strings.TrimSpace(raw)
if raw == "" || strings.HasPrefix(raw, "#") {
return true
}
u, err := url.Parse(raw)
if err != nil {
return false
}
if u.Scheme == "" {
// Relative path or fragment — no scheme, no script surface.
return true
}
s := strings.ToLower(u.Scheme)
return s == "http" || s == "https"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Protocol-relative URLs slip through isSafeSVGURL.

url.Parse("//evil.com/x") returns Scheme=="" with Host set, so the u.Scheme == "" branch returns true. That allows network-relative URLs in href/xlink:href, which a browser will resolve against the page's scheme. The CSP blocks subresource loads, but navigation from an in-document <a xlink:href="//evil.com"> click isn't covered by default-src 'none' and would just redirect.

If protocol-relative isn't intentionally supported, consider rejecting authority-bearing URLs without an explicit http/https scheme:

🛡️ Suggested fix
 	if u.Scheme == "" {
-		// Relative path or fragment — no scheme, no script surface.
-		return true
+		// Reject protocol-relative ("//host/...") URLs; a relative
+		// path/fragment has no Host.
+		return u.Host == ""
 	}

And add a test case:

{"protocol-relative", "//evil.com/x.png", false},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/service/org_logo.go` around lines 263 - 278, The isSafeSVGURL function
currently treats URLs with empty Scheme as safe, which lets protocol-relative
URLs (e.g. "//evil.com/x") slip through because url.Parse sets Scheme=="" but
Host is populated; update isSafeSVGURL to reject authority-bearing URLs with an
empty scheme by returning false when u.Scheme=="" && u.Host!="" (i.e., treat
network-relative URLs as unsafe), keep allowing relative paths/fragments (empty
scheme and empty host), and retain explicit allow for "http"/"https" schemes;
also add the test case {"protocol-relative", "//evil.com/x.png", false} to the
existing tests to cover this scenario.

Comment thread server/service/org_logo.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
server/fleet/org_logo.go (2)

147-153: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

SMIL animation elements can mutate validated attributes after parse-time — add to blocklist.

<set>, <animate>, <animateTransform>, and <animateMotion> are well-documented sanitizer-bypass vectors. The validator inspects href/src statically, but SMIL can rewrite a sibling's href to any URL via to/values/from attributes — which aren't checked. Example currently passing validation:

<svg xmlns="http://www.w3.org/2000/svg">
  <a href="#safe">
    <set attributeName="href" to="javascript:alert(1)"/>
    <rect width="10" height="10"/>
  </a>
</svg>

The CSP blocks inline javascript: execution on click in modern browsers, but <set attributeName="href" to="https://attacker.example"> isn't a script-src violation, so users can still be silently redirected.

🛡️ Suggested addition
 var disallowedSVGElements = map[string]struct{}{
 	"script":        {},
 	"foreignobject": {},
 	"iframe":        {},
 	"object":        {},
 	"embed":         {},
+	// SMIL animation elements can rewrite href/xlink:href at runtime,
+	// defeating the static href/src checks below.
+	"set":              {},
+	"animate":          {},
+	"animatetransform": {},
+	"animatemotion":    {},
 }

Add a corresponding regression test in TestValidateOrgLogoBytesSVG.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/fleet/org_logo.go` around lines 147 - 153, The SVG sanitizer's
disallowedSVGElements map in org_logo.go misses SMIL animation elements that can
mutate attributes at runtime; add "set", "animate", "animateTransform", and
"animateMotion" to disallowedSVGElements and update the validator accordingly,
then add a regression unit test in TestValidateOrgLogoBytesSVG that verifies an
SVG using <set>/<animate*> to change href/src is rejected; reference the
disallowedSVGElements map and TestValidateOrgLogoBytesSVG when making the
changes.

127-142: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Protocol-relative URLs (//host/path) bypass isSafeSVGURL.

url.Parse("//evil.com/x") returns Scheme=="" with Host populated, so the empty-scheme branch returns true. This allows network-relative URLs to slip into rendered xlink:href values. The CSP blocks subresource loads, but in-document <a xlink:href="//evil.com"> navigation isn't covered by default-src 'none'.

🛡️ Suggested fix
 	if u.Scheme == "" {
-		// Relative path or fragment — no scheme, no script surface.
-		return true
+		// Reject protocol-relative ("//host/...") URLs; a true relative
+		// path/fragment has no Host.
+		return u.Host == ""
 	}

Add a corresponding test case {"protocol-relative", "//evil.com/x.png", false} to TestValidateOrgLogoBytesSVG.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/fleet/org_logo.go` around lines 127 - 142, isSafeSVGURL currently
treats URLs with an empty scheme as safe, which lets protocol-relative URLs like
"//evil.com/x" bypass checks; update isSafeSVGURL to treat protocol-relative
URLs as unsafe by returning false when u.Scheme == "" but u.Host != "" (i.e.,
detect empty scheme with a populated Host and reject it), and add a unit test
entry {"protocol-relative", "//evil.com/x.png", false} to
TestValidateOrgLogoBytesSVG to cover this case.
🧹 Nitpick comments (1)
server/fleet/org_logo_test.go (1)

73-104: ⚡ Quick win

Consider extending the URL scheme matrix.

While here, two additions would harden the existing matrix once the isSafeSVGURL protocol-relative fix lands (see server/fleet/org_logo.go review):

  • {"protocol-relative", "//evil.com/x.png", false} — guards the Scheme=="" branch.
  • A tab/newline injection case like "java\tscript:alert(1)"url.Parse may still parse a recognizable scheme depending on browser leniency, worth a regression bar.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/fleet/org_logo_test.go` around lines 73 - 104, Update the href/src URL
schemes matrix in the test under t.Run("href/src URL schemes") to include two
more failing cases that guard the Scheme=="" and whitespace-trick parsing: add
{"protocol-relative", "//evil.com/x.png", false} and a control with embedded
whitespace such as {"tab/newline injection", "java\tscript:alert(1)", false};
ensure these are passed into ValidateOrgLogoBytes (the same table-driven loop
around ValidateOrgLogoBytes) and keep the same assertions (require.Error for
false cases and require.NoError for true cases), optionally checking the error
contains the existing indicator (e.g., "fragment") so the regression will fail
if isSafeSVGURL or ValidateOrgLogoBytes incorrectly treat protocol-relative or
whitespace-split schemes as safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@server/fleet/org_logo.go`:
- Around line 147-153: The SVG sanitizer's disallowedSVGElements map in
org_logo.go misses SMIL animation elements that can mutate attributes at
runtime; add "set", "animate", "animateTransform", and "animateMotion" to
disallowedSVGElements and update the validator accordingly, then add a
regression unit test in TestValidateOrgLogoBytesSVG that verifies an SVG using
<set>/<animate*> to change href/src is rejected; reference the
disallowedSVGElements map and TestValidateOrgLogoBytesSVG when making the
changes.
- Around line 127-142: isSafeSVGURL currently treats URLs with an empty scheme
as safe, which lets protocol-relative URLs like "//evil.com/x" bypass checks;
update isSafeSVGURL to treat protocol-relative URLs as unsafe by returning false
when u.Scheme == "" but u.Host != "" (i.e., detect empty scheme with a populated
Host and reject it), and add a unit test entry {"protocol-relative",
"//evil.com/x.png", false} to TestValidateOrgLogoBytesSVG to cover this case.

---

Nitpick comments:
In `@server/fleet/org_logo_test.go`:
- Around line 73-104: Update the href/src URL schemes matrix in the test under
t.Run("href/src URL schemes") to include two more failing cases that guard the
Scheme=="" and whitespace-trick parsing: add {"protocol-relative",
"//evil.com/x.png", false} and a control with embedded whitespace such as
{"tab/newline injection", "java\tscript:alert(1)", false}; ensure these are
passed into ValidateOrgLogoBytes (the same table-driven loop around
ValidateOrgLogoBytes) and keep the same assertions (require.Error for false
cases and require.NoError for true cases), optionally checking the error
contains the existing indicator (e.g., "fragment") so the regression will fail
if isSafeSVGURL or ValidateOrgLogoBytes incorrectly treat protocol-relative or
whitespace-split schemes as safe.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8038329d-7731-4e76-9a30-376a7732c573

📥 Commits

Reviewing files that changed from the base of the PR and between 5092846 and 3a0b832.

⛔ Files ignored due to path filters (1)
  • server/fleet/testdata/icons/org_logo_css.svg is excluded by !**/*.svg
📒 Files selected for processing (3)
  • server/fleet/org_logo.go
  • server/fleet/org_logo_test.go
  • server/service/org_logo.go

- Block <set>, <animate>, <animateTransform>, <animateMotion>: SMIL can
  rewrite an ancestor's href/xlink:href to javascript:... at runtime,
  bypassing the static href allowlist (CodeRabbit feedback).
- Reject protocol-relative URLs ("//host/x") in href/src: url.Parse
  returns Scheme=="" with Host populated, which the previous check
  treated as safe (CodeRabbit feedback).
- Update client_appconfig_test.go assertions to match the new
  "PNG, JPEG, WebP, or SVG" error message that came in with the merge.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/fleet/org_logo.go
Comment on lines +194 to +198
for _, attr := range t.Attr {
attrName := strings.ToLower(attr.Name.Local)
// on* (onclick, onload, …) is SVG's main XSS vector.
if strings.HasPrefix(attrName, "on") {
return &BadRequestError{Message: "SVG event-handler attributes are not allowed"}
Comment thread server/fleet/org_logo.go
Comment on lines +182 to +184
switch t := tok.(type) {
case xml.StartElement:
name := strings.ToLower(t.Name.Local)
Comment thread frontend/utilities/file/orgLogoFile.ts Outdated
Comment on lines +54 to +60
// SVG is text — search the sniff window for "<svg" (case-insensitive).
// Real SVGs put the root tag near the top, after at most an XML
// declaration, comments, or a DOCTYPE. Strict safety checks happen
// server-side; the FE check is just for early UX feedback.
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
if (/<svg\b/i.test(text)) {
return "svg";
Comment on lines 53 to +57
})
t.Run("rejects unknown format", func(t *testing.T) {
err := validateOrgLogoFile(writeTempFile(t, "logo.txt", []byte("not an image")))
require.Error(t, err)
assert.ErrorContains(t, err, "PNG, JPEG, or WebP")
assert.ErrorContains(t, err, "PNG, JPEG, WebP, or SVG")
Copilot review follow-ups:

- xml:base routed through isSafeSVGURL: a hostile base ("javascript:")
  would re-anchor every relative href in the subtree and bypass the
  static href allowlist.
- Reject XML processing instructions other than <?xml ...?>: notably
  <?xml-stylesheet href=...?> pulls external resources when the SVG
  loads as a document.
- FE detection now requires <svg as the first start tag (after BOM,
  whitespace, XML decl, comments, DOCTYPE, PIs). Previously any text
  containing "<svg" anywhere was accepted, so an HTML file with an
  inline <svg> only got caught after the upload round-trip.
- Add positive `accepts svg` case to TestValidateOrgLogoFile so the
  gitops preflight path keeps exercising SVG.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
server/fleet/org_logo.go (1)

106-125: 💤 Low value

Consider checking image magic bytes before the SVG sniff to stay consistent with ContentTypeForOrgLogo.

ContentTypeForOrgLogo checks PNG/JPEG/WebP magic first and only then falls back to looksLikeSVG. ValidateOrgLogoBytes does the opposite: it routes anything containing <svg in the first 512 bytes to validateSVG, even when the leading bytes are clearly a PNG/JPEG/WebP. Because looksLikeSVG uses bytes.Contains rather than a leading match, a legitimate PNG/JPEG/WebP that happens to embed the literal <svg in early text/EXIF metadata would now be misrouted into XML parsing and rejected, despite image.DecodeConfig being able to accept it.

Mirroring the order used in ContentTypeForOrgLogo (or reusing it) keeps the two entry points in agreement and removes the edge case.

♻️ Sketch
 func ValidateOrgLogoBytes(b []byte) error {
 	if int64(len(b)) > OrgLogoMaxFileSize {
 		return &BadRequestError{Message: "logo must be 100KB or less"}
 	}
-	if looksLikeSVG(b) {
-		return validateSVG(b)
-	}
+	// Prefer magic-byte detection so binary images carrying "<svg" in
+	// metadata aren't misrouted into the XML validator.
+	switch {
+	case bytes.HasPrefix(b, orgLogoPNGMagic),
+		bytes.HasPrefix(b, orgLogoJPEGMagic),
+		hasWebPMagic(b):
+		// fall through to image.DecodeConfig
+	case looksLikeSVG(b):
+		return validateSVG(b)
+	}
 	_, format, err := image.DecodeConfig(bytes.NewReader(b))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/fleet/org_logo.go` around lines 106 - 125, ValidateOrgLogoBytes
currently checks looksLikeSVG before image magic bytes which can misroute valid
PNG/JPEG/WebP that contain "<svg" in metadata; update ValidateOrgLogoBytes to
first detect PNG/JPEG/WebP using the same magic-byte logic as
ContentTypeForOrgLogo (or call ContentTypeForOrgLogo) and only call
looksLikeSVG/validateSVG if magic-byte detection fails, keeping
image.DecodeConfig as the fallback for raster formats and preserving existing
error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@server/fleet/org_logo.go`:
- Around line 106-125: ValidateOrgLogoBytes currently checks looksLikeSVG before
image magic bytes which can misroute valid PNG/JPEG/WebP that contain "<svg" in
metadata; update ValidateOrgLogoBytes to first detect PNG/JPEG/WebP using the
same magic-byte logic as ContentTypeForOrgLogo (or call ContentTypeForOrgLogo)
and only call looksLikeSVG/validateSVG if magic-byte detection fails, keeping
image.DecodeConfig as the fallback for raster formats and preserving existing
error handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 83a0a772-c870-423a-903b-17ea557dd545

📥 Commits

Reviewing files that changed from the base of the PR and between 0e3ff1e and 3098b67.

📒 Files selected for processing (4)
  • frontend/utilities/file/orgLogoFile.ts
  • server/fleet/org_logo.go
  • server/fleet/org_logo_test.go
  • server/service/client_appconfig_test.go
✅ Files skipped from review due to trivial changes (1)
  • server/service/client_appconfig_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/utilities/file/orgLogoFile.ts
  • server/fleet/org_logo_test.go

@nulmete
nulmete merged commit fd3ec5a into main May 6, 2026
58 checks passed
@nulmete
nulmete deleted the add-svg-support-custom-logos branch May 6, 2026 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants