Skip to content

Commit 7945e89

Browse files
committed
fix(ios): canonical status string in block error
PolicyMutator's block error interpolated the Swift enum case via \(status); Approov.string(from:) yields the canonical MITM_DETECTED-style name, matching the rest of the iOS layer and Android's enum toString(). Also reword the MITM_DETECTED/REJECTED mask security warning in REFERENCE.md, USAGE.md and CHANGELOG.md to state the request proceeds without proof of attestation (no valid Approov token). (Copilot review, PR #32.)
1 parent 1f3da04 commit 7945e89

4 files changed

Lines changed: 4 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## [3.5.16] - 2026-07-22
44
- **iOS `approov.plist` is now optional (fixes a launch crash)**: When Approov is initialized natively from a bundled `approov.config`, the iOS module no longer raises `ApproovPropsNotFound` at launch if a bundled `approov.plist` is absent. It now logs and runs with default properties, matching the Android layer where a missing `approov.props` is tolerated. This lets an `approov.config`-only native integration run without shipping an empty placeholder `approov.plist`. A present-but-unreadable plist still raises, since that indicates a genuine misconfiguration. (Reported by PropertyGuru.)
5-
- **Added — JS-selectable request policy (security-relevant)**: New `ApproovService.setServiceMutatorType(mask, { sign })` method plus `ApproovService.ReturnDecision` bit flags and `ApproovService.MutatorPreset` presets let an app choose the per-status proceed/block policy — and whether the outbound request is still HTTP Message Signed — entirely from JavaScript, with no native mutator code. The `mask` lists which Approov **failure** statuses may proceed; a failure status not in the mask blocks the request, surfacing identically on both platforms as a failed `fetch()` (`IOException` / `Network request failed` on Android, an `NSError` failure on iOS). Named presets (`DEFAULT`, `ALWAYS_PROCEED`, `PROCEED_IF_UNAVAILABLE`, `PROCEED_DEV_CLEARTEXT`) cover the common cases; the development-only `PROCEED_DEV_CLEARTEXT` forwards Metro's cleartext dev bundle, which the SDK reports as `BAD_URL` (issue #30). **Security caveat:** proceeding on `MITM_DETECTED` or `REJECTED` disables the protection those statuses provide, so keep those bits out of production masks unless you fully intend to. Pair with `setUseApproovStatusIfNoToken(true)` to surface the failure status to the backend in the token header. Behaviour is identical on Android and iOS.
5+
- **Added — JS-selectable request policy (security-relevant)**: New `ApproovService.setServiceMutatorType(mask, { sign })` method plus `ApproovService.ReturnDecision` bit flags and `ApproovService.MutatorPreset` presets let an app choose the per-status proceed/block policy — and whether the outbound request is still HTTP Message Signed — entirely from JavaScript, with no native mutator code. The `mask` lists which Approov **failure** statuses may proceed; a failure status not in the mask blocks the request, surfacing identically on both platforms as a failed `fetch()` (`IOException` / `Network request failed` on Android, an `NSError` failure on iOS). Named presets (`DEFAULT`, `ALWAYS_PROCEED`, `PROCEED_IF_UNAVAILABLE`, `PROCEED_DEV_CLEARTEXT`) cover the common cases; the development-only `PROCEED_DEV_CLEARTEXT` forwards Metro's cleartext dev bundle, which the SDK reports as `BAD_URL` (issue #30). **Security caveat:** proceeding on `MITM_DETECTED` or `REJECTED` removes Approov's protection for those cases — the request is sent without proof of attestation (no valid Approov token), so keep those bits out of production masks unless you fully intend to. Pair with `setUseApproovStatusIfNoToken(true)` to surface the failure status to the backend in the token header. Behaviour is identical on Android and iOS.
66
- **Fixed — service mutator now resets on re-initialization (security-relevant)**: A configuration-change re-initialization previously left a custom service mutator installed; it now resets to the built-in message-signing default on both Android and iOS whenever the config actually changes. A same-config re-init still preserves the installed mutator, so re-apply `setServiceMutatorType` after a config-change re-init if you still need a custom policy.
77
- **Hardened — `setServiceMutatorType` mask validation (security-relevant)**: The proceed bitmask is bridged from JavaScript as a floating-point number and is now rejected on both Android and iOS unless it is a finite, integral, 32-bit value. A fractional, `NaN`/`Infinity`, or out-of-range mask would previously have been silently truncated or coerced by the native narrowing cast, potentially installing a policy other than the one intended; such inputs now reject the promise instead of being applied. Covered by new Android and iOS native regression tests. (Copilot review, PR #32.)
88

REFERENCE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ ApproovService.setServiceMutatorType(mask: number, options?: { sign?: boolean })
304304
| `PROCEED_DEV_CLEARTEXT` | Proceed on `BAD_URL`, forwarding non-`https` traffic; development only. |
305305

306306
> [!WARNING]
307-
> Including `MITM_DETECTED` or `REJECTED` in the mask disables the protection those statuses provide: a man-in-the-middle interception, or a failed/rejected attestation, would no longer block the request. Prefer the named presets, and only put these bits in a production mask deliberately.
307+
> Including `MITM_DETECTED` or `REJECTED` in the mask removes Approov's protection for those cases: the request proceeds **without proof of attestation** (no valid Approov token). With `MITM_DETECTED` masked, a request the SDK reports as man-in-the-middle intercepted is still sent; with `REJECTED` masked, a request from an app that failed attestation (for example tampered, repackaged, or running in a compromised environment) is still sent. Prefer the named presets, and only set these bits in a production mask deliberately.
308308
309309
On iOS, `NO_NETWORK_PERMISSION` and `MISSING_LIB_DEPENDENCY` have no equivalent Approov status and are inert (harmless) if included.
310310

USAGE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ await ApproovService.setServiceMutatorType(RD.NO_APPROOV_SERVICE, { sign: false
293293

294294
`ApproovService.ReturnDecision` exposes one bit flag per maskable failure status: `NO_APPROOV_SERVICE`, `BAD_URL`, `MITM_DETECTED`, `NO_NETWORK`, `POOR_NETWORK`, `REJECTED`, `UNKNOWN_KEY`, `INTERNAL_ERROR`, `NO_NETWORK_PERMISSION`, `MISSING_LIB_DEPENDENCY`, and `DISABLED`. Only these **failure** statuses are maskable. `SUCCESS` always proceeds (with the signed token added), and `UNKNOWN_URL` / `UNPROTECTED_URL` always proceed unmodified (forwarded without a token); those three are never maskable and are not exposed as flags.
295295

296-
> ⚠️ **Security warning.** The mask decides which failures the app will *tolerate*. Putting **`MITM_DETECTED`** or **`REJECTED`** in the mask disables the protection those statuses provide: a man-in-the-middle interception, or a failed/rejected attestation, will no longer block the request. Most apps should use the named presets. Reach for the raw `ReturnDecision` bitmask only deliberately, and **never** ship `MITM_DETECTED` or `REJECTED` in a production mask unless you fully intend to accept that risk.
296+
> ⚠️ **Security warning.** The mask decides which failures the app will *tolerate*. Putting **`MITM_DETECTED`** or **`REJECTED`** in the mask removes Approov's protection for those cases: the request proceeds **without proof of attestation** (no valid Approov token). Masking `MITM_DETECTED` lets a request the SDK reports as man-in-the-middle intercepted go out anyway; masking `REJECTED` lets a request from an app that failed attestation (tampered, repackaged, or running in a compromised environment) go out anyway. Most apps should use the named presets. Reach for the raw `ReturnDecision` bitmask only deliberately, and **never** ship `MITM_DETECTED` or `REJECTED` in a production mask unless you fully intend to accept that risk.
297297
298298
**Reporting the status to your backend.** When a failure status is in the mask, the request proceeds. If you also enable `ApproovService.setUseApproovStatusIfNoToken(true)`, the fetch-status string (for example `MITM_DETECTED`) is written into the token header so the backend can see why no real token was sent; otherwise the token header is emitted empty.
299299

ios/ApproovURLSession/PolicyMutator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public final class PolicyMutator: ApproovServiceMutator, CustomStringConvertible
210210
case .forward:
211211
return false
212212
case .block:
213-
throw ApproovServiceError.permanentError(message: "PolicyMutator blocked: \(status)")
213+
throw ApproovServiceError.permanentError(message: "PolicyMutator blocked: " + Approov.string(from: status))
214214
}
215215
}
216216

0 commit comments

Comments
 (0)