Skip to content

Commit ae0edc5

Browse files
XseuguhAlmouro
andauthored
feat: disable default iOS URLCache (#26)
## What is the problem On iOS, each request is cached in plain text within the app’s file system. A malicious user with root access could access this cache and extract sensitive data, such as credentials from a login endpoint. [More details on this article](https://medium.com/@mehran.kmlf/guarding-your-apps-secrets-the-hidden-dangers-of-cache-db-in-ios-f3f07d5febed) ## Proposal Deactivate the URLCache and clear the existing cache => **Will it break things ?** On react native app, caching is mainly done on the JS side, this native cache does not seem to be used ## How to reproduce - Launch your app - Open the files associated to this app (for example using `open $(xcrun simctl get_app_container booted <your.bundle.id> data)`) - Go to `Library/Caches/<your.bundle.id>` - Open the `Cache.db` <table> <thead> <tr> <th scope="col">Before</th> <th scope="col">After</th> </tr> </thead> <tbody> <tr> <td> https://github.com/user-attachments/assets/5c8c6894-08d5-49e7-8a66-a95de3cb4d94 </td > <td> https://github.com/user-attachments/assets/1f4ea1dd-e885-488f-b0d0-5a5d76313b06 </td > </tr> </tbody> </table> # TODO Blocking the merge: - [x] add a flag to enable/disable the functionality (disable by default) - [x] complete README with an `Experimental` tag To go further: - [ ] investigate more deeply the full impact of fully disabling the cache (webview, assets, ...) --------- Co-authored-by: Alexandre Moureaux <Almouro@users.noreply.github.com>
1 parent c1a861c commit ae0edc5

File tree

7 files changed

+108
-5
lines changed

7 files changed

+108
-5
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- [Prevent "recent screenshots"](#prevent-recent-screenshots)
1515
- [Configuration](#configuration-2)
1616
- [Safe Keyboard Detector](#safe-keyboard-detector)
17+
- [[EXPERIMENTAL - iOS only] Disable Default Caching in `Cache.db`](#experimental---ios-only-disable-default-caching-in-cachedb)
1718
- [Contributing](#contributing)
1819
- [👉 About BAM](#-about-bam)
1920

@@ -182,6 +183,39 @@ if (!isInDefaultSafeList) {
182183
SafeKeyboardDetector.showInputMethodPicker(); // can only be called on Android
183184
```
184185

186+
## [EXPERIMENTAL - iOS only] Disable Default Caching in `Cache.db`
187+
> ⚠️ **DISCLAIMER:** This experimental feature may impact app behavior. Use it at your own risk. Disabling caching can cause unexpected issues.
188+
>
189+
> **Possible side effects:**
190+
> - Slower performance due to lack of cached responses
191+
> - Higher network usage from repeated requests
192+
> - Crashes in components expecting cached data
193+
> - Features failing in offline mode
194+
195+
> **🥷 Threat:** On iOS, every `NSURL` request may be cached by default in `Cache.db`, potentially storing sensitive data unless explicitly disabled. This can lead to unintentional data leaks.
196+
197+
Mitigating this threat is achieved by:
198+
199+
- Fully clearing the existing cache
200+
- Remove the cache by setting it to an empty cache:
201+
202+
```swift
203+
URLCache.shared = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
204+
```
205+
### Configuration
206+
If you want to enable this functionality, it need to be enabled in the app configuration file (by default it's disabled)
207+
208+
```jsonc
209+
[
210+
"@bam.tech/react-native-app-security",
211+
{
212+
"disableCache": {
213+
"ios": { "enabled": true },
214+
}
215+
}
216+
]
217+
```
218+
185219
# Contributing
186220

187221
Contributions are welcome. See the [Expo modules docs](https://docs.expo.dev/modules/get-started/) for information on how to build/run/develop on the project.

example/app.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ const config: ExpoConfig = {
4949
enabled: true,
5050
},
5151
},
52+
disableCache: {
53+
ios: {
54+
enabled: true,
55+
},
56+
},
5257
},
5358
],
5459
"expo-router",

example/app/index.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ export default function App() {
4141
onPress={() => SafeKeyboardDetector.showInputMethodPicker()}
4242
/>
4343
) : null}
44+
{Platform.OS === "ios" ? (
45+
<Button title="fake login route call" onPress={callLoginRoute} />
46+
) : null}
4447
</View>
4548
);
4649
}
@@ -114,3 +117,18 @@ const checkIsKeyboardSafe = () => {
114117
console.log(SafeKeyboardDetector.getCurrentInputMethodInfo().inputMethodId);
115118
console.warn("is Keyboard safe", isKeyboardSafe);
116119
};
120+
121+
const callLoginRoute = async () => {
122+
try {
123+
await fetch("http://localhost:8081/login", {
124+
method: "POST",
125+
headers: {
126+
"Content-Type": "application/json",
127+
},
128+
body: JSON.stringify({
129+
email: "example@myMail.com",
130+
password: "a super strong password",
131+
}),
132+
});
133+
} catch (error) {}
134+
};

ios/RNASAppLifecyleDelegate.swift

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,22 @@ public class RNASAppLifecycleDelegate: ExpoAppDelegateSubscriber {
1010
return window
1111
}()
1212

13-
public func applicationDidFinishLaunching(_ application: UIApplication) {
14-
if(!isPreventRecentScreenshotsEnabled()) {
15-
return
13+
public func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
14+
if(isPreventRecentScreenshotsEnabled()) {
15+
application.ignoreSnapshotOnNextApplicationLaunch()
1616
}
17-
18-
application.ignoreSnapshotOnNextApplicationLaunch()
17+
18+
if(isDisablingCacheEnabled()){
19+
clearAndDisableCache()
20+
}
21+
22+
23+
return true
24+
}
25+
26+
private func clearAndDisableCache() {
27+
URLCache.shared.removeAllCachedResponses()
28+
URLCache.shared = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
1929
}
2030

2131
public func applicationWillResignActive(_ application: UIApplication) {
@@ -41,3 +51,9 @@ func isPreventRecentScreenshotsEnabled() -> Bool {
4151
return false
4252
}
4353

54+
func isDisablingCacheEnabled() -> Bool {
55+
if let value = Bundle.main.object(forInfoDictionaryKey: "RNAS_DISABLE_CACHE") as? Bool {
56+
return value
57+
}
58+
return false
59+
}

plugin/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { ConfigPlugin } from "@expo/config-plugins";
22
import { RNASConfig } from "./types";
3+
import withDisableCache from "./withDisableCache";
34
import withpreventRecentScreenshots from "./withPreventRecentScreenshots";
45
import withSSLPinning from "./withSSLPinning";
56

67
const withRNAS: ConfigPlugin<RNASConfig> = (config, props) => {
78
config = withSSLPinning(config, props.sslPinning);
89

910
config = withpreventRecentScreenshots(config, props.preventRecentScreenshots);
11+
config = withDisableCache(config, props.disableCache);
1012

1113
return config;
1214
};

plugin/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ export type RNASConfig = {
66
ios?: { enabled: boolean };
77
android?: { enabled: boolean };
88
};
9+
disableCache?: {
10+
ios?: { enabled: boolean };
11+
};
912
};

plugin/src/withDisableCache.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ConfigPlugin, withInfoPlist } from "@expo/config-plugins";
2+
import { RNASConfig } from "./types";
3+
4+
type Props = RNASConfig["preventRecentScreenshots"];
5+
6+
const withDisableCache: ConfigPlugin<Props> = (config, props) => {
7+
config = withInfoPlist(config, (config) => {
8+
const infoPlist = config.modResults;
9+
10+
const isEnabled = props?.ios?.enabled ?? false;
11+
12+
if (!isEnabled) {
13+
delete infoPlist.RNAS_DISABLE_CACHE;
14+
return config;
15+
}
16+
17+
infoPlist.RNAS_DISABLE_CACHE = true;
18+
19+
return config;
20+
});
21+
22+
return config;
23+
};
24+
25+
export default withDisableCache;

0 commit comments

Comments
 (0)