From 97df01a6b6ee7e22fc375f5b4174ed62df531949 Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Mon, 22 Jun 2026 18:40:32 +0000 Subject: [PATCH 1/2] Reject Manifest V2 extensions at upload with a clear error Chromium no longer loads Manifest V2 extensions. Without an explicit check, an uploaded MV2 extension is accepted and silently fails to load in the browser. Validate manifest_version on upload and return a 400 explaining that the extension must be Manifest V3. Co-Authored-By: Claude Opus 4.7 --- server/cmd/api/api/chromium.go | 8 +++++++ server/lib/policy/policy.go | 23 ++++++++++++++++++++ server/lib/policy/policy_test.go | 36 ++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/server/cmd/api/api/chromium.go b/server/cmd/api/api/chromium.go index bd4c4c7a..05c9dcf4 100644 --- a/server/cmd/api/api/chromium.go +++ b/server/cmd/api/api/chromium.go @@ -208,6 +208,14 @@ func (s *ApiService) applyExtensionZipItems(ctx context.Context, items []extensi return "invalid zip file", nil } + manifestVersion, found, err := policy.ManifestVersion(filepath.Join(dest, "manifest.json")) + if err != nil { + return fmt.Sprintf("extension %s has an invalid manifest.json: %v", p.name, err), nil + } + if found && manifestVersion > 0 && manifestVersion < 3 { + return fmt.Sprintf("extension %s uses Manifest V%d, which Chromium no longer supports; upgrade it to Manifest V3", p.name, manifestVersion), nil + } + updateXMLPath := filepath.Join(dest, "update.xml") if err := policy.RewriteUpdateXMLUrls(updateXMLPath, p.name); err != nil { log.Warn("failed to rewrite update.xml URLs", "error", err, "extension", p.name) diff --git a/server/lib/policy/policy.go b/server/lib/policy/policy.go index 036cb222..cdbd13e0 100644 --- a/server/lib/policy/policy.go +++ b/server/lib/policy/policy.go @@ -271,6 +271,29 @@ func (p *Policy) RequiresEnterprisePolicy(manifestPath string) (bool, error) { return false, nil } +// ManifestVersion reads the manifest_version field from an extension's manifest.json. +// It returns the version and whether a manifest.json was present. A missing manifest.json +// is not an error: extensions installed via update.xml + .crx may not ship an unpacked +// manifest, and those are validated by Chromium itself. +func ManifestVersion(manifestPath string) (version int, found bool, err error) { + data, err := os.ReadFile(manifestPath) + if err != nil { + if os.IsNotExist(err) { + return 0, false, nil + } + return 0, false, err + } + + var m struct { + ManifestVersion int `json:"manifest_version"` + } + if err := json.Unmarshal(data, &m); err != nil { + return 0, false, fmt.Errorf("failed to parse manifest.json: %w", err) + } + + return m.ManifestVersion, true, nil +} + // updateManifest represents the Chrome extension update manifest XML structure type updateManifest struct { XMLName xml.Name `xml:"gupdate"` diff --git a/server/lib/policy/policy_test.go b/server/lib/policy/policy_test.go index f33bfb32..d8928f00 100644 --- a/server/lib/policy/policy_test.go +++ b/server/lib/policy/policy_test.go @@ -2,6 +2,8 @@ package policy import ( "encoding/json" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -168,3 +170,37 @@ func TestPolicy_EmptyPolicy(t *testing.T) { // Should have empty ExtensionSettings as null/missing assert.Nil(t, result["ExtensionSettings"]) } + +func TestManifestVersion(t *testing.T) { + dir := t.TempDir() + write := func(name, contents string) string { + path := filepath.Join(dir, name) + require.NoError(t, os.WriteFile(path, []byte(contents), 0o644)) + return path + } + + t.Run("manifest v3", func(t *testing.T) { + version, found, err := ManifestVersion(write("mv3.json", `{"manifest_version": 3, "name": "x"}`)) + require.NoError(t, err) + assert.True(t, found) + assert.Equal(t, 3, version) + }) + + t.Run("manifest v2", func(t *testing.T) { + version, found, err := ManifestVersion(write("mv2.json", `{"manifest_version": 2, "name": "x"}`)) + require.NoError(t, err) + assert.True(t, found) + assert.Equal(t, 2, version) + }) + + t.Run("missing file is not an error", func(t *testing.T) { + _, found, err := ManifestVersion(filepath.Join(dir, "does-not-exist.json")) + require.NoError(t, err) + assert.False(t, found) + }) + + t.Run("invalid json", func(t *testing.T) { + _, _, err := ManifestVersion(write("bad.json", `{not json`)) + require.Error(t, err) + }) +} From cf25794a5646034c6f7dc64d0349df3438b7d832 Mon Sep 17 00:00:00 2001 From: Sayan- <1415138+Sayan-@users.noreply.github.com> Date: Tue, 23 Jun 2026 05:07:18 +0000 Subject: [PATCH 2/2] Return 500 for manifest I/O errors, 400 only for malformed manifests --- server/cmd/api/api/chromium.go | 7 ++++++- server/lib/policy/policy.go | 7 ++++++- server/lib/policy/policy_test.go | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/server/cmd/api/api/chromium.go b/server/cmd/api/api/chromium.go index 05c9dcf4..8a246985 100644 --- a/server/cmd/api/api/chromium.go +++ b/server/cmd/api/api/chromium.go @@ -2,6 +2,7 @@ package api import ( "context" + "errors" "fmt" "io" "mime/multipart" @@ -210,7 +211,11 @@ func (s *ApiService) applyExtensionZipItems(ctx context.Context, items []extensi manifestVersion, found, err := policy.ManifestVersion(filepath.Join(dest, "manifest.json")) if err != nil { - return fmt.Sprintf("extension %s has an invalid manifest.json: %v", p.name, err), nil + if errors.Is(err, policy.ErrInvalidManifest) { + return fmt.Sprintf("extension %s has an invalid manifest.json: %v", p.name, err), nil + } + log.Error("failed to read extension manifest", "error", err, "extension", p.name) + return "", fmt.Errorf("failed to read extension manifest: %w", err) } if found && manifestVersion > 0 && manifestVersion < 3 { return fmt.Sprintf("extension %s uses Manifest V%d, which Chromium no longer supports; upgrade it to Manifest V3", p.name, manifestVersion), nil diff --git a/server/lib/policy/policy.go b/server/lib/policy/policy.go index cdbd13e0..c255d3eb 100644 --- a/server/lib/policy/policy.go +++ b/server/lib/policy/policy.go @@ -3,6 +3,7 @@ package policy import ( "encoding/json" "encoding/xml" + "errors" "fmt" "os" "regexp" @@ -11,6 +12,10 @@ import ( "sync" ) +// ErrInvalidManifest indicates a manifest.json that exists but could not be parsed. +// It distinguishes a malformed manifest (a client error) from an I/O failure reading it. +var ErrInvalidManifest = errors.New("invalid manifest.json") + const PolicyPath = "/etc/chromium/policies/managed/policy.json" // Chrome extension IDs are 32 lowercase a-p characters @@ -288,7 +293,7 @@ func ManifestVersion(manifestPath string) (version int, found bool, err error) { ManifestVersion int `json:"manifest_version"` } if err := json.Unmarshal(data, &m); err != nil { - return 0, false, fmt.Errorf("failed to parse manifest.json: %w", err) + return 0, false, fmt.Errorf("%w: %v", ErrInvalidManifest, err) } return m.ManifestVersion, true, nil diff --git a/server/lib/policy/policy_test.go b/server/lib/policy/policy_test.go index d8928f00..4a70a9e4 100644 --- a/server/lib/policy/policy_test.go +++ b/server/lib/policy/policy_test.go @@ -202,5 +202,6 @@ func TestManifestVersion(t *testing.T) { t.Run("invalid json", func(t *testing.T) { _, _, err := ManifestVersion(write("bad.json", `{not json`)) require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidManifest) }) }