Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions server/cmd/api/api/chromium.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"context"
"errors"
"fmt"
"io"
"mime/multipart"
Expand Down Expand Up @@ -208,6 +209,18 @@ 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 {
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
}

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)
Expand Down
28 changes: 28 additions & 0 deletions server/lib/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package policy
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"os"
"regexp"
Expand All @@ -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
Expand Down Expand Up @@ -271,6 +276,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("%w: %v", ErrInvalidManifest, err)
}

return m.ManifestVersion, true, nil
}

// updateManifest represents the Chrome extension update manifest XML structure
type updateManifest struct {
XMLName xml.Name `xml:"gupdate"`
Expand Down
37 changes: 37 additions & 0 deletions server/lib/policy/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package policy

import (
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -168,3 +170,38 @@ 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)
assert.ErrorIs(t, err, ErrInvalidManifest)
})
}
Loading