App Store review: Guideline 2.5.2
An app that downloads web assets may receive a question or rejection under App Review Guideline 2.5.2. This page describes the plugin's verifiable controls and a conservative way to operate it. It is not legal advice and does not predict an App Review outcome. Apple evaluates the submitted app, its behavior, its metadata, and the content available during review.
Current Apple requirements
Read the current Apple text when preparing each submission. As of August 4, 2026, the relevant provisions are:
- Apple Developer Program License Agreement §3.3.1(B), Executable Code. It says an application may not download or install executable code except as provided there. Downloaded interpreted code must not change the app's primary purpose through features inconsistent with its intended and advertised purpose, bypass OS signing, sandbox, or security features, or create a store or storefront for other applications.
- App Review Guideline 2.5.2. It requires apps to remain self-contained in their bundles, use their designated container area, and not download, install, or execute code that introduces or changes app features or functionality.
The agreement accepted in the developer account is controlling and can change. Do not rely on an older section number or on a summary in this page without checking Apple's current text.
Operational release boundary
Use OTA only for fixes and content changes within functionality and purposes already presented to and reviewed by App Review. Submit a new App Store build before distributing any of the following:
- a new feature or a material change to an existing feature;
- a new or changed purpose for the app or for collected data;
- a new permission request or a change in how a permission is used;
- a new native capability, entitlement, framework, or Capacitor plugin;
- a store, storefront, catalog, or behavior that offers other apps; or
- a change that makes the App Store description, screenshots, privacy answers, review notes, or other metadata inaccurate.
The plugin's file checks cannot determine whether JavaScript changes a feature, purpose, or storefront behavior. That semantic boundary belongs in your release approval process. Keep a record for each OTA release identifying the reviewed functionality it fixes or updates.
What the submitted plugin does
The current plugin downloads a plain, signed ZIP into the app's private container. It does not decrypt the bundle. Before publication it applies the following controls on iOS and Android.
Download and origin checks
- Bundle URLs must use HTTPS, except loopback URLs used for local development.
- The host must match
updateUrl,statsUrl, orallowedDownloadHosts. - Redirect destinations are checked against the same scheme and host rules.
- The downloaded ZIP must match its SHA-256 checksum.
- The ZIP must have a valid signature for the configured public key. A missing
key or signature is rejected with
SIGNATURE_REQUIRED.
The private signing key remains in the publisher's build environment. These controls authenticate bytes and constrain their source; they do not decide whether an OTA release changes reviewed functionality.
Private staging and publication
Extraction occurs under a randomly named private staging directory inside the app's container. A candidate does not become a stored bundle until extraction and all validation complete and the staging directory is renamed into the published bundle location.
Rejected executable bytes can exist transiently only in that private staging directory. The extension check rejects a disallowed filename before its file is written. For an allowlisted filename, the extractor writes the candidate while hashing it, then checks its leading bytes for known Mach-O, ELF, DEX, Java class, and PE signatures. If that or any later validation fails, the staging directory is removed. The candidate is never published, selected, served by the web view, or activated.
That distinction matters when describing the implementation: do not say that no executable-looking bytes can ever be written to disk. They may be written temporarily during validation, but not outside private staging and not into a servable bundle.
File and archive checks
Every file path, case-insensitively, must end in one of these extensions:
html htm css js mjs cjs json map webmanifest
svg png jpg jpeg gif webp avif ico bmp
woff woff2 ttf otf eot
mp3 mp4 webm ogg wav m4a
txt xml csv md
wasm
Directory entries are exempt from the extension check. Only the final extension
counts, so bundle.js.map is allowed and library.dylib or
library.dylib.js.so is not.
The extractor also:
- rejects absolute paths,
..traversal, and entries resolving outside the staging directory; - applies 100 MB per-entry and 500 MB total extracted-size limits;
- skips ZIP symlink entries on iOS; Android's platform ZIP API materializes them as ordinary files rather than links, after which the normal path, extension, and manifest checks apply;
- requires
.dya-manifest.jsonwithmanifestVersion: 1and the requested bundle version; - verifies each extracted file's SHA-256 against the manifest and rejects both unlisted files and missing manifest entries; and
- requires a servable
index.htmlat the archive root or in one top-level directory before publication.
These checks are useful, reviewable evidence about the bytes accepted for publication. They are not a substitute for the operational release boundary.
Runtime boundary
Published assets run in Capacitor's web view through setServerBasePath. The
plugin does not link a downloaded native library or add entitlements,
permissions, native frameworks, or native plugins. Those native components are
fixed in the App Store build.
JavaScript can still call native capabilities already present in that build. Therefore, the absence of new native code does not by itself show that an OTA release leaves features unchanged. Review both the OTA diff and the capabilities already embedded in the submitted binary.
Dynamic-dispatch symbols
Capacitor's bridge uses dynamic dispatch to route JavaScript plugin calls to
native plugin classes. For example, @capacitor/ios 8.4.2 contains
NSClassFromString, NSSelectorFromString, and
method_exchangeImplementations in framework source.
If a rejection names one of these APIs, inspect the exact archive submitted to Apple and identify every occurrence. State that a symbol comes from Capacitor only after verifying its object file or framework path. Also search the host app and every other dependency; do not copy a framework explanation that is not true of the submitted archive.
The current plugin does not add a hidden gesture, debug overlay, or touch
listener. The older enableTestGesture option and testGestureTrigger event
were removed in plugin 0.4.0. Check the version actually embedded in the archive
and disclose any diagnostic or developer surface supplied by the host app.
Preparing the submission
The bundled privacy manifest declares linked, non-tracking Performance Data and Other Diagnostic
Data for the lifecycle durations, byte counts, and failure reasons sent by default analytics.
Keep App Store Connect answers aligned with the shipped analyticsEnabled setting. Device ID is
still sent for update functionality when analytics is disabled.
Before submitting:
- Build the exact archive intended for App Store Connect and confirm its embedded plugin version and configuration.
- Make the built-in web bundle fully usable without downloading an OTA bundle.
- Keep
resetWhenUpdateenabled unless you have reviewed and documented a different store-update behavior. - Inspect a representative OTA ZIP, its manifest, checksum, and signature.
- Verify that recent and queued OTA releases are only fixes or content changes within already-reviewed functionality.
- Submit a new App Store build for any new or changed feature, purpose, permission, native capability, or storefront behavior.
- Keep the App Store description, screenshots, privacy disclosures, and review notes accurate for both the built-in experience and available OTA content.
- Put a test account, reproduction steps, and a sample bundle URL in review notes when they help App Review exercise the update flow.
These steps improve the accuracy and verifiability of a submission. They do not guarantee acceptance.
Responding to a 2.5.2 question or rejection
Reply through App Store Connect with facts verified against the submitted archive. Explain:
- what is downloaded;
- where it is staged and when it can be published;
- what validates it;
- how it is loaded at runtime;
- the operational limit on OTA releases; and
- which changes your team sends through a new App Store build.
Use this template as a checklist, not as a representation that every sentence is true for your app:
Thank you for the review. We verified the following against build [version] ([build number]).
The app downloads plain, signed ZIP archives containing its own HTML, CSS, JavaScript, media, and related web assets. We operate OTA releases only for fixes and content changes within functionality and purposes already presented for App Review. New or changed features, purposes, permissions, native capabilities, or storefront behavior are released through a new App Store build.
A candidate archive is checksum- and signature-verified, then extracted only in a private staging directory inside the app container. The extractor checks path containment, an allowlist of web-asset filename extensions, known native executable headers, size limits, and a per-file SHA-256 manifest. A file with a known executable header may be written transiently while it is inspected in private staging; on rejection, staging is removed and the candidate is never published, served, or activated. Only a fully validated directory containing
index.htmlis renamed into the published bundle location.Published assets are loaded by Capacitor in
WKWebViewusingsetServerBasePath. Downloaded content is not linked as a native library and cannot add entitlements, permissions, native frameworks, or native plugins to the submitted binary.We use these controls and our release policy in light of Apple Developer Program License Agreement §3.3.1(B) and App Review Guideline 2.5.2. We can provide the current bundle URL, manifest, release diff, and a demonstration of the update flow.
[If verified in the submitted archive:] The named dynamic-dispatch symbols are in the open-source Capacitor framework at [paths]. Our archive inspection found [describe findings for the app and other dependencies].
Remove bracketed text, unsupported statements, and controls your configuration does not use. If the review message raises a different behavior, answer that behavior directly rather than forcing it into this template.
Bundle encryption and signing
Current releases do not support encrypted OTA bundles. There is no
encryptionPrivateKey option, sessionKey response field, or decryption step in
the plugin. Bundles are plain ZIP files so a reviewer with the URL can inspect
their contents.
Signing authenticates origin and integrity; it does not make an OTA release appropriate under Apple's feature and functionality rules. Keep the signing controls and the release-policy review as separate checks.
The plugin uses platform cryptographic APIs for hashing and signature verification. Export-control answers depend on the complete host app and the current rules, so verify them for each submission rather than inheriting a blanket answer from this SDK documentation.
Related
- Update lifecycle — how a bundle is verified, published, activated, and rolled back
- API reference — the allowlist, magic table, and manifest shape
- Configuration —
publicKey,allowedDownloadHosts,resetWhenUpdate - Setup — installing and configuring the plugin