Files
Jonathan Katz 2c383d7b8d Differentiate between ipa and other zip file types in ExtractInstallerMetadata (#48802)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #48102

Changes:
- Renames `ExtractIPAMetadata` to `ExtractZIPMetadata` because the magic
bytes for zip based installers (.ipa, .msix, .zip, etc) are the same so
any zip file reaches it. If the zip does not contain an `Info.plist`
file it will now fail with `ErrInvalidType`.
- Did **NOT** make typeFromBytes return "zip" instead of "ipa" because
meta.Extension is set from that which has downstream effects.
- Added test files 
The actual error message is still just "invalid file type". 

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] Timeouts are implemented and retries are limited to avoid infinite
loops
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually
- Tested adding a valid `.ipa`, a macos FMA that uses a .zip file
(alt-tab/darwin), and a windows FMA that uses a .zip file
(vnc-server/windows).
- Tested an msix file (renamed or not) cannot be uploaded or edited for
an existing msi installer
  - Also tested the same things via GitOps
  

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved installer type detection so ZIP-based packages are less
likely to be misidentified.
* Fixed an error message that incorrectly referred to the wrong file
type when detection fails.
* MSIX packages are now reported more accurately when they don’t match
IPA parsing rules.
* **Refactor**
  * Cleaned up installer metadata handling for ZIP-based archives.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-06 18:18:16 -04:00

81 lines
2.0 KiB
Go

package file
import (
"archive/zip"
"crypto/sha256"
"errors"
"fmt"
"io"
"strings"
"github.com/fleetdm/fleet/v4/server/fleet"
"howett.net/plist"
)
// ExtractZIPMetadata extracts the metadata from a zip file for an Apple app
func ExtractZIPMetadata(tfr *fleet.TempFileReader) (*InstallerMetadata, error) {
h := sha256.New()
_, _ = io.Copy(h, tfr) // writes to a hash cannot fail
if err := tfr.Rewind(); err != nil {
return nil, fmt.Errorf("rewind reader: %w", err)
}
r, err := zip.OpenReader(tfr.Name())
if err != nil {
return nil, err
}
var plistData struct {
BundleID string `plist:"CFBundleIdentifier"`
Name string `plist:"CFBundleName"`
Version string `plist:"CFBundleShortVersionString"`
RequiresIPhoneOS bool `plist:"LSRequiresIPhoneOS"`
}
var hasInfoPlist, isIPA bool
for _, f := range r.File {
// Matches any Info.plist and the last wins, so a nested framework or
// extension plist can override the app's own plist.
if strings.Contains(f.Name, "Info.plist") {
// Get data from plist file
archiveFile, err := f.Open()
if err != nil {
return nil, fmt.Errorf("could not open archive %s: %w", f.Name, err)
}
defer archiveFile.Close()
rawData, err := io.ReadAll(archiveFile)
if err != nil {
return nil, err
}
_, err = plist.Unmarshal(rawData, &plistData)
if err != nil {
return nil, err
}
hasInfoPlist = true
// LSRequiresIPhoneOS is set on iOS/iPadOS apps and never on macOS
// apps, so it is probably an .ipa
if plistData.RequiresIPhoneOS {
isIPA = true
}
}
}
if !hasInfoPlist || !isIPA {
// non Apple file formats based on zip are not supported (msix)
return nil, ErrInvalidType
}
if plistData.BundleID == "" {
return nil, errors.New("couldn't find bundle identifier for in-house app")
}
return &InstallerMetadata{
BundleIdentifier: plistData.BundleID,
SHASum: h.Sum(nil),
PackageIDs: []string{plistData.BundleID},
Name: plistData.Name,
Version: plistData.Version,
}, nil
}