DEV Community

Cover image for OWASP Mobile Top 10 — M6: Inadequate Privacy Controls
Barış Kandemir
Barış Kandemir

Posted on

OWASP Mobile Top 10 — M6: Inadequate Privacy Controls

Welcome to the sixth article in our OWASP Mobile Top 10 2024 series! In previous articles we covered M1 through M5. Today we reach the most unusual item on the list: the only risk with a LOW technical impact and a SEVERE business impact.


Introduction

M6 is the most unusual item in the OWASP Mobile Top 10. The other nine describe technical flaws: a query that wasn't parameterized, a certificate that wasn't validated, a token stored insecurely. M6 asks a different question:

"Why are you collecting this data at all?"

And if the answer is "because we might need it," you already have an M6 violation.

The interesting part is that M6's technical impact is rated LOW while its business impact is SEVERE. It's the only inversion on the list. Your code can work perfectly, nothing crashes, no system goes down — and your company can still face millions in fines.

A specific situation for React Native developers

React Native apps carry extra M6 risk because:

  • The dependency chain runs deep. One npm install can pull in three native SDKs, each sending its own telemetry.
  • The JS side is very "talkative." The console.log habit, crash reporting integrations, and dev tooling make PII leakage easy.
  • Platform requirements keep changing. Apple's Privacy Manifest requirement and Google Play's Data Safety form are declarations that must be filled in correctly — and they must match your app's actual behaviour.

OWASP Assessment

Metric Value Meaning
Exploitability AVERAGE Requires breaching another layer first
Prevalence COMMON In nearly every application
Detectability EASY Watching traffic/logs is enough
Technical Impact LOW The system keeps running
Business Impact SEVERE Legal penalties, lawsuits, reputational loss

⚠️ Read that table twice. Technical impact LOW, business impact SEVERE. This explains why engineering teams keep deferring M6: nothing breaks, no alarm goes off. The problem is that the bill arrives much later and much larger.

Why is the technical impact low? Privacy violations usually have little technical impact on the system as a whole. Only if the PII includes information like authentication data can it affect certain global security properties, such as traceability.


What PII Is and Why It's Valuable

Privacy controls are concerned with protecting Personally Identifiable Information (PII): names and addresses, credit card information, e-mail and IP addresses, information about health, religion, sexuality and political opinions.

Why do attackers want this data?

An attacker could:

  • Impersonate the victim to commit fraud
  • Misuse the victim's payment data
  • Blackmail the victim with sensitive information
  • Harm the victim by destroying or manipulating their critical data

In general, PII could either be leaked (a violation of confidentiality), manipulated (violation of integrity), or destroyed/blocked (violation of availability).

The "our app doesn't collect PII" fallacy

An app can only be vulnerable to Inadequate Privacy Controls if it processes some form of personally identifiable information. And this is almost always the case: client apps' IP addresses visible to a server, logs of the app's usage, and metadata sent with crash reports or analytics are PII that apply to most apps.

Here's the PII of an app that "doesn't collect PII":

  • 🌐 IP address — in server logs, on every request
  • 📱 Device identifiers — IDFA, Android ID, install ID
  • 📊 Usage logs — which screen, when, how long
  • 💥 Crash metadata — device model, OS, app state
  • 🕐 Timestamps — a behavioural profile can be built
  • 🌍 Language and locale — location inference
  • 📶 Network carrier — geographic and socioeconomic signal

Combined, these are usually enough to identify one person — which is, by definition, PII.

Almost all apps process some kind of PII. Many even collect and process more than they need to fulfil their purpose, which makes them more attractive as a target without business needs.


How M6 Differs from the Other Items

This is the key to understanding M6: it isn't a separate bug class, it's a lens applied to the other items.

Given an app that uses PII, it might expose it like any other sensitive data. This most notably happens through:

  • Insecure data storage and communication (M5, M9)
  • Data access with insecure authentication and authorization (M3, M1)
  • Insider attacks on the app's sandbox (M2, M4, M8)

In other words, M6 = the other items × personal data:

Combination Result
M5 (Insecure communication) + PII Personal data leaks in transit
M9 (Insecure storage) + PII Personal data plain on device
M3 (Insecure auth) + PII Access to someone else's data
M4 (Missing validation) + PII Leak to logs/clipboard/URL
M2 (Supply chain) + PII An SDK's hidden telemetry

⚠️ But M6 has a question of its own: "Could we have avoided collecting this data entirely?"

That last line is critical. M5 says "encrypt the data," M9 says "store it securely." M6 proposes something more radical: something that does not exist cannot be attacked.


Attack Vectors

Typical sources for PII are well protected — for example the sandbox of the app, the network communication with the server, the app's logs and backups. Some have less protection but are still hard to access, like URL query parameters and clipboard content.

Obtaining PII thus requires the attacker to first breach security on another level. Attackers could eavesdrop on the network communication, access the file system, clipboard, or logs with a trojan, or get their hands on the mobile device and create a backup to analyze.

Well protected (another layer must be breached first):

  • App sandbox
  • Network communication with the server
  • App logs
  • Device backups

Less protected (but still hard to access):

  • URL query parameters
  • Clipboard content

Since PII is just data that can be stored, processed, and transmitted by all means available on mobile devices, the possibilities to extract or manipulate it are manifold.


OWASP Example Attack Scenarios

OWASP gives three concrete scenarios for M6 — and all three are extremely common in React Native projects.

Scenario #1 — Inadequate sanitization of logs and error messages

Reporting of logs and exceptions is essential for quality assurance of a productive app. Crash reports and other usage data help developers fix bugs and learn how their app is used. However, logs and error messages might contain PII if the developers chose to include this data in log or error messages. Also, third-party libraries might include PII in their error messages and logs as well.

An example of a frequent issue are database exceptions that reveal part of the query or result. This will most likely be visible to any platform provider used for collecting and evaluating crash reports. It might also become visible to the user if the error is displayed on screen, or to attackers who can read device logs.

💡 The React Native equivalent: every place you wrote console.log(user) and every exception you send to Sentry/Crashlytics. Details below.

Scenario #2 — Using PII in URL query parameters

URL query parameters are often used to transmit request arguments to a server. However, URL query parameters are visible at least in the server logs, but often also in website analytics and possibly in the local browser history. So sensitive information should never be transmitted as query parameters. Instead, they should be sent as a header or part of the body.

// ❌ Visible in server logs, CDN logs, analytics
GET /api/users?email=jane@example.com&phone=5551234567

// ✅ In the body or a header
POST /api/users/lookup
{ "email": "jane@example.com" }
Enter fullscreen mode Exit fullscreen mode

Scenario #3 — Exclusion of personal data in backups / not setting hasFragileUserData

Most PII processed by an app is stored in its sandbox. The app should explicitly configure what data to include in device backups. An attacker might obtain a device and create a backup, or get a backup from another source, from which the sandbox content could be extracted.

Alternatively, by setting hasFragileUserData to true on Android, an app may preserve its data upon uninstallation. An attacker who manages to install a malicious app with the same package id later can access this data.

Hence, both settings should be explicitly set for apps to make the developers' intent transparent and to control the information flow through backups or between subsequent installations of an app.


Technical and Business Impacts

Technical Impacts — LOW

Privacy violations usually have little technical impact on the system as a whole. Only if the PII includes information like authentication data can it affect certain global security properties, e.g., traceability.

If user data is manipulated it might render the system unusable for that user. Through ill-formed data, the backend may also be disturbed if it is missing proper sanitization and exception handling.

Business Impacts — SEVERE

The extent and severity of the business impact strongly depends on the number of affected users, the criticality of the affected data, and the data protection regulations that apply where the violation happened.

⚖️ Violation of legal regulations

Regulations are the biggest issue regarding privacy controls. Relevant regulations with known sanctions: GDPR (Europe), CCPA (California, US), PDPA (Singapore), PIPEDA (Canada), LGPD (Brazil), Data Protection Act 2018 (UK), POPIA (South Africa), PDPL (China).

💰 Financial damage due to victims' lawsuits

Whoever is personally affected by a privacy violation might sue the app provider that let the violation happen. These lawsuits might be successful, depending on the legal regulations that apply and the ability of the provider to show that they had adequate and up to date protection mechanisms in place.

📉 Reputational damage

If a privacy violation affects users on a large scale, it is likely published in media, generating negative publicity for the provider. As a consequence, sales and usage for the app and even other, unrelated products of the same provider might drop.

🔓 Loss or theft of PII

Actual information stolen might be misused, even for attacks on the provider of the app. For example, specific user data could be used to employ a social engineering attack on the provider by impersonating a victim.

⚠️ Note the phrasing about lawsuits: "depending on... the ability of the provider to show that they had adequate and up to date protection mechanisms in place." Applying protection isn't enough; you have to be able to document it. That makes M6 a documentation problem too.


Prevention Strategies: Data Minimization

OWASP's proposed solution for M6 is fundamentally different from the other items:

Something that does not exist cannot be attacked, so the safest approach to prevent privacy violations is to minimize the amount and variety of PII that is processed.

This requires full awareness of all PII assets in a given app. With that awareness, the following six questions should be assessed:

  1. Is all PII processed really necessary? (name and address, gender, age...)
  2. Can some of the PII be replaced by less critical information? (fine-grained location → coarse-grained)
  3. Can some of the PII be reduced? (location updates every hour instead of every minute)
  4. Can some of the PII be anonymized or blurred? (by hashing, bucketing, or adding noise)
  5. Can some of the PII be deleted after an expiration period? (only keep health data of the last week)
  6. Can users consent to optional PII usage? (receive a better service but be aware of the added risk)

The remaining PII should not be stored or transferred unless absolutely necessary. If it must be stored or transferred, access must be protected with proper authentication and possibly authorization.

Defense in depth should also be considered for particularly critical data. For example, health data may be encrypted with a key sealed in the device's TPM in addition to its storage in the app's sandbox. So if an attacker manages to circumvent the sandbox restrictions, the data is still not readable.

Threat modeling can be used to determine the most likely ways that privacy violations may occur in a given app. The effort of securing PII could then be focused on these.

Static and dynamic security checking tools might reveal common pitfalls, like logging of sensitive data or leakage to clipboard or URL query parameters.


React Native Specific Security

1. Build a PII inventory

Minimization starts with awareness. If you don't know which personal data enters your app, from where, and where it goes, you have nothing to minimize.

// privacy/dataInventory.js
// This file isn't code, it's documentation. But sitting next to the code keeps it alive.

export const PII_INVENTORY = {
  email: {
    purpose: 'Account identity and communication',
    collectedAt: 'Registration screen',
    storedIn: ['Keychain (inside token)', 'Backend'],
    sharedWith: ['Backend API'],
    retention: 'Until account deletion',
    legalBasis: 'Performance of a contract',
    required: true,
  },
  preciseLocation: {
    purpose: 'Show nearby stores',
    collectedAt: 'Store finder screen',
    storedIn: ['In memory, not persisted'],
    sharedWith: ['Maps SDK'],
    retention: 'Session only',
    legalBasis: 'Explicit consent',
    required: false,
    // 🔍 QUESTION 2: Would coarse location be enough?
    minimizationNote: 'City-level may suffice — evaluate',
  },
  deviceId: {
    purpose: 'Grouping crash reports',
    collectedAt: 'App launch',
    storedIn: ['Sentry'],
    sharedWith: ['Sentry (third party)'],
    retention: '90 days',
    legalBasis: 'Legitimate interest',
    required: false,
    // 🔍 QUESTION 4: Can it be hashed?
    minimizationNote: 'Use a hash instead of the raw ID',
  },
};
Enter fullscreen mode Exit fullscreen mode

This inventory does three jobs: (1) it gives you a list to apply the minimization questions to, (2) it's the source when filling in the Google Play Data Safety form and Apple Privacy Manifest, (3) it lets you document the claim that you "had adequate protection mechanisms" in an audit.


2. Permission minimization and just-in-time requests

The biggest privacy mistake is requesting every permission at app launch.

// ❌ BAD — ask for everything at launch, user has no idea why
useEffect(() => {
  requestCameraPermission();
  requestLocationPermission();
  requestContactsPermission();
  requestNotificationPermission();
}, []);
Enter fullscreen mode Exit fullscreen mode
// ✅ GOOD — ask at the moment of need, with context
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { Platform } from 'react-native';

const LOCATION_PERMISSION = Platform.select({
  ios: PERMISSIONS.IOS.LOCATION_WHEN_IN_USE,
  android: PERMISSIONS.ANDROID.ACCESS_COARSE_LOCATION,  // ✅ COARSE, not FINE
});

export async function requestLocationWithContext() {
  const status = await check(LOCATION_PERMISSION);

  if (status === RESULTS.GRANTED) return true;

  if (status === RESULTS.DENIED) {
    // Explain why BEFORE the system prompt
    const userAgreed = await showRationale({
      title: "Let's find nearby stores",
      message:
        'We use your location only to show the stores closest to you. ' +
        'It never leaves your device and is not stored.',
      confirmText: 'Continue',
      cancelText: "I'll pick a city",
    });

    if (!userAgreed) {
      // ✅ Accept the refusal and offer an alternative
      return navigateToManualCitySelection();
    }

    const result = await request(LOCATION_PERMISSION);
    return result === RESULTS.GRANTED;
  }

  if (status === RESULTS.BLOCKED) {
    // ✅ Don't push the user to Settings, offer an alternative
    return navigateToManualCitySelection();
  }

  return false;
}
Enter fullscreen mode Exit fullscreen mode

Minimization in permission choice

Need ❌ Excessive ✅ Minimum
Nearby stores ACCESS_FINE_LOCATION ACCESS_COARSE_LOCATION
Profile photo READ_EXTERNAL_STORAGE Photo Picker (no permission)
Continuous location ACCESS_BACKGROUND_LOCATION WHEN_IN_USE
Finding friends READ_CONTACTS Manual invite by the user
QR scanning Camera + gallery Camera only

The Android 13+ and iOS 14+ photo pickers require no permission — the user shares only the photo they selected. That's far less data access than requesting READ_EXTERNAL_STORAGE.


3. Log and crash report hygiene (OWASP Scenario #1)

This is the most common M6 violation in React Native projects.

// ❌ All of these leak PII
console.log('User logged in:', user);            // the whole user object
console.log('Response:', JSON.stringify(res));   // API response
console.log('Token:', accessToken);              // credentials
console.error('Query failed:', sqlQuery);        // query + parameters
Enter fullscreen mode Exit fullscreen mode

console.log output is readable via adb logcat on Android and Console.app on iOS. Anyone with device access — and in some cases another app — can see it.

Step 1: Strip console in production

// babel.config.js
module.exports = {
  presets: ['module:@react-native/babel-preset'],
  env: {
    production: {
      plugins: [
        ['transform-remove-console', { exclude: ['error', 'warn'] }],
      ],
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

But since you're excluding error and warn, make sure you don't write PII there either.

Step 2: Write a redaction layer

// utils/redact.js

const PII_KEYS = [
  'email', 'phone', 'phoneNumber', 'password', 'token', 'accessToken',
  'refreshToken', 'ssn', 'iban', 'cardNumber', 'cvv',
  'address', 'latitude', 'longitude', 'birthDate', 'fullName',
];

const EMAIL_RE = /[\w.+-]+@[\w-]+\.[\w.]+/g;
// Phone numbers need either a + country code or separators. A bare run of
// digits is deliberately NOT matched: Date.now() is 13 digits, and so are
// plenty of order IDs and counters — matching those would silently corrupt
// your logs while looking like it worked.
const PHONE_RE = /\+\d{1,3}[\s.-]?\d{2,4}[\s.-]?\d{3}[\s.-]?\d{2,4}\b|\b\d{3}[\s.-]\d{3}[\s.-]\d{4}\b/g;
const IBAN_RE = /\b[A-Z]{2}\d{2}[\sA-Z0-9]{10,30}\b/g;

export function redact(value, depth = 0) {
  if (depth > 6) return '[deep]';

  if (typeof value === 'string') {
    return value
      .replace(EMAIL_RE, '[EMAIL]')
      .replace(PHONE_RE, '[PHONE]')
      .replace(IBAN_RE, '[IBAN]');
  }

  if (Array.isArray(value)) {
    return value.map((v) => redact(v, depth + 1));
  }

  if (value && typeof value === 'object') {
    const out = {};
    for (const [key, val] of Object.entries(value)) {
      const isPii = PII_KEYS.some((k) =>
        key.toLowerCase().includes(k.toLowerCase())
      );
      out[key] = isPii ? '[REDACTED]' : redact(val, depth + 1);
    }
    return out;
  }

  return value;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Filter crash reporting

// services/sentry.js
import * as Sentry from '@sentry/react-native';
import { redact } from '../utils/redact';

Sentry.init({
  dsn: 'https://...',

  // ✅ Don't send the user's IP
  sendDefaultPii: false,

  beforeSend(event) {
    // Scrub exception messages
    if (event.exception?.values) {
      event.exception.values = event.exception.values.map((ex) => ({
        ...ex,
        value: typeof ex.value === 'string' ? redact(ex.value) : ex.value,
      }));
    }

    // Scrub extra and contexts
    if (event.extra) event.extra = redact(event.extra);
    if (event.contexts) event.contexts = redact(event.contexts);

    // Minimize the user object — no email/IP
    if (event.user) {
      event.user = { id: event.user.id };  // opaque ID only
    }

    // Scrub request data
    if (event.request) {
      delete event.request.cookies;
      delete event.request.headers?.Authorization;
      if (event.request.data) event.request.data = redact(event.request.data);
    }

    return event;
  },

  beforeBreadcrumb(breadcrumb) {
    // Strip query strings from network breadcrumb URLs
    if (breadcrumb.category === 'xhr' || breadcrumb.category === 'fetch') {
      if (breadcrumb.data?.url) {
        breadcrumb.data.url = breadcrumb.data.url.split('?')[0];
      }
    }
    // Scrub console breadcrumbs
    if (breadcrumb.category === 'console' && breadcrumb.message) {
      breadcrumb.message = redact(breadcrumb.message);
    }
    return breadcrumb;
  },
});
Enter fullscreen mode Exit fullscreen mode

⚠️ Third-party libraries log PII too. OWASP states this explicitly. Database exceptions, HTTP library errors, and SDKs' own logs are outside your control. beforeSend catches a lot of that — but not all of it. Sentry's own docs are clear that in the React Native SDK, beforeSend only filters events generated from the JavaScript layer; native events from Android and iOS code are not passed through it. So a crash inside a native SDK can carry PII straight past your filter. Treat beforeSend as a strong layer, not a complete one, and keep PII out of native-facing calls in the first place.


4. Keep PII out of URLs (OWASP Scenario #2)

// ❌ Visible in server logs, the CDN, analytics, proxies
await apiClient.get(`/users/search?email=${email}&phone=${phone}`);

// ✅ Send in the body
await apiClient.post('/users/search', { email, phone });
Enter fullscreen mode Exit fullscreen mode

You can catch this with an interceptor:

// Warn during development if a URL carries PII
const PII_PARAM_NAMES = ['email', 'phone', 'token', 'ssn', 'iban', 'name'];

apiClient.interceptors.request.use((config) => {
  if (__DEV__ && config.url?.includes('?')) {
    const query = config.url.split('?')[1];
    const params = new URLSearchParams(query);

    for (const key of params.keys()) {
      if (PII_PARAM_NAMES.some((p) => key.toLowerCase().includes(p))) {
        console.warn(
          `⚠️ Possible PII in URL query parameter: "${key}" — move it to the body`
        );
      }
    }
  }
  return config;
});
Enter fullscreen mode Exit fullscreen mode

The same rule applies to deep links. We covered deep link validation in M4; the additional question here is: what are you carrying in that link? Deep link URLs persist in OS logs and sometimes browser history.


5. Backup and uninstall behaviour (OWASP Scenario #3)

Android

<!-- android/app/src/main/AndroidManifest.xml -->
<application
    android:allowBackup="false"
    android:hasFragileUserData="false"
    ...>
Enter fullscreen mode Exit fullscreen mode

OWASP recommends setting both explicitly — leaving them to defaults makes your intent unclear.

  • allowBackup="false" → app data is not included in device backups
  • hasFragileUserData="false" → data is deleted when the app is uninstalled

If you set hasFragileUserData="true", an attacker who installs a malicious app with the same package id can access your old data.

If you do need backups, keep allowBackup="true" and be selective instead — the rules files only take effect when backup is on, so pointing at them while backup is disabled does nothing:

<application
    android:allowBackup="true"
    android:hasFragileUserData="false"
    android:fullBackupContent="@xml/backup_rules"
    android:dataExtractionRules="@xml/data_extraction_rules"
    ...>
Enter fullscreen mode Exit fullscreen mode

You need both attributes because they cover different versions: fullBackupContent applies up to API 30, and dataExtractionRules takes over on API 31+ (and also governs device-to-device transfer, which is separate from cloud backup).

<!-- android/app/src/main/res/xml/backup_rules.xml — API ≤ 30 -->
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
  <!-- Default: back up nothing -->
  <exclude domain="sharedpref" path="." />
  <exclude domain="database" path="." />
  <exclude domain="file" path="." />

  <!-- Only non-sensitive preferences -->
  <include domain="sharedpref" path="app_theme_prefs.xml" />
</full-backup-content>
Enter fullscreen mode Exit fullscreen mode
<!-- android/app/src/main/res/xml/data_extraction_rules.xml — API 31+ -->
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
  <cloud-backup>
    <exclude domain="sharedpref" path="." />
    <exclude domain="database" path="." />
    <exclude domain="file" path="." />
    <include domain="sharedpref" path="app_theme_prefs.xml" />
  </cloud-backup>
  <!-- Device-to-device transfer is a separate decision -->
  <device-transfer>
    <exclude domain="sharedpref" path="." />
    <exclude domain="database" path="." />
    <exclude domain="file" path="." />
  </device-transfer>
</data-extraction-rules>
Enter fullscreen mode Exit fullscreen mode

iOS

iOS backs up the Documents directory to iCloud by default. The underlying mechanism for opting out is the NSURLIsExcludedFromBackupKey resource value — but be aware that popular JS file-system libraries don't all expose it, so check your library's API rather than assuming a helper exists. If yours doesn't, you need a small native module.

Two approaches that don't require one:

import RNFS from 'react-native-fs';

// ✅ Option 1: Caches — never backed up, but the system may purge it.
// Good for regenerable data (thumbnails, downloaded media, parsed feeds).
const cachePath = `${RNFS.CachesDirectoryPath}/${filename}`;
await RNFS.writeFile(cachePath, content, 'utf8');
Enter fullscreen mode Exit fullscreen mode
// ✅ Option 2: for secrets, don't put them in a file at all.
// Keychain with a ThisDeviceOnly accessibility class is excluded from
// backups by the OS — no extra configuration needed.
import * as Keychain from 'react-native-keychain';

await Keychain.setGenericPassword('auth', token, {
  accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
Enter fullscreen mode Exit fullscreen mode

That second one connects back to M3: the ThisDeviceOnly variants we used for token storage were already solving a backup-exposure problem, not just an access-control one.


6. Clipboard leakage

The clipboard is one of the channels OWASP places in the "less protected" category. Anything you copy is accessible to other apps.

import Clipboard from '@react-native-clipboard/clipboard';

// ❌ Leaving sensitive data on the clipboard
Clipboard.setString(otpCode);

// ✅ Copy, then clear after a delay
async function copyTemporarily(value, ttlMs = 30000) {
  Clipboard.setString(value);

  setTimeout(async () => {
    // hasString() checks for content without reading it — on iOS 14+ a
    // read can surface the "pasted from" banner, so avoid getString() here.
    const hasContent = await Clipboard.hasString();
    if (hasContent) {
      Clipboard.setString('');
    }
  }, ttlMs);
}
Enter fullscreen mode Exit fullscreen mode

There's a real trade-off in that last block. You can't tell whether the clipboard still holds your value without reading it back, and reading it back is the thing you're trying to avoid. Clearing unconditionally means you might wipe something the user copied in the meantime. For a 30-second OTP that's usually the right call; for longer-lived values, consider not copying to the clipboard at all and offering an autofill or in-app paste instead.

You can disable copying entirely on sensitive fields:

<TextInput
  value={cardNumber}
  contextMenuHidden={true}      // ✅ No copy/paste menu
  selectTextOnFocus={false}
  secureTextEntry
/>
Enter fullscreen mode Exit fullscreen mode

iOS 14+ and Android 12+ show the user a notification on clipboard reads — that's how you can notice an SDK reading the clipboard without telling you.


7. Screenshot and app switcher masking

When the user leaves the app, the OS takes a screenshot and shows it in the app switcher. That image is written to disk.

import { useEffect } from 'react';
import ScreenGuardModule from 'react-native-screenguard';

function BankingScreen() {
  useEffect(() => {
    (async () => {
      // ✅ Required from v2.0.0 onwards — register() alone will not work
      await ScreenGuardModule.initSettings({
        displayScreenGuardOverlay: true,
        timeAfterResume: 2000,
      });

      await ScreenGuardModule.register({ backgroundColor: '#000000' });
    })();

    return () => {
      ScreenGuardModule.unregister();
    };
  }, []);

  return <View>{/* sensitive content */}</View>;
}
Enter fullscreen mode Exit fullscreen mode

This also blocks screenshots and screen recording (via FLAG_SECURE on Android). Check the version you install: the v1.x API took a colour string directly and had no initSettings, so older snippets you find online won't work on v2 and vice versa.


8. App Tracking Transparency (iOS)

Since iOS 14.5, you must ask the user for permission before collecting tracking identifiers like the IDFA.

import {
  getTrackingStatus,
  requestTrackingPermission,
} from 'react-native-tracking-transparency';

export async function initTracking() {
  const status = await getTrackingStatus();

  if (status === 'not-determined') {
    // ✅ Give context before the prompt
    await showTrackingRationale();

    const newStatus = await requestTrackingPermission();

    if (newStatus === 'authorized') {
      enableAdTracking();
    } else {
      // ✅ Actually honour the refusal — turn the SDKs off too
      disableAdTracking();
    }
    return;
  }

  if (status === 'authorized' || status === 'unavailable') {
    enableAdTracking();
  } else {
    disableAdTracking();
  }
}
Enter fullscreen mode Exit fullscreen mode
<!-- ios/YourApp/Info.plist -->
<key>NSUserTrackingUsageDescription</key>
<string>We'd like to use your advertising identifier to show you more relevant ads. If you decline, the app works exactly the same.</string>
Enter fullscreen mode Exit fullscreen mode

⚠️ The most critical point: when the status is denied, actually disable your ad SDKs. Asking for permission and ignoring the refusal is a heavier violation than never asking — both legally and under App Store policy.


9. Platform declarations: Privacy Manifest and Data Safety

These two aren't code, they're declarations. But filled in wrongly, your app gets rejected or creates a compliance problem later.

iOS — PrivacyInfo.xcprivacy

Apple stopped accepting app submissions without a privacy manifest as of May 1, 2024. The file declares why the app calls certain APIs Apple considers sensitive — currently these cover accessing UserDefaults, file timestamps, system boot time, disk space, and the active keyboard. Apple describes this as an open list that may expand.

React Native generates a template during the pod install step. You may still need to add the reasons for APIs used by your dependencies to your own PrivacyInfo.xcprivacy.

In Expo this is managed through the app config:

// app.config.js
export default {
  expo: {
    ios: {
      privacyManifests: {
        NSPrivacyAccessedAPITypes: [
          {
            NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryUserDefaults',
            NSPrivacyAccessedAPITypeReasons: ['CA92.1'],
          },
          {
            NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryFileTimestamp',
            NSPrivacyAccessedAPITypeReasons: ['C617.1'],
          },
        ],
        NSPrivacyCollectedDataTypes: [
          {
            NSPrivacyCollectedDataType: 'NSPrivacyCollectedDataTypeEmailAddress',
            NSPrivacyCollectedDataTypeLinked: true,
            NSPrivacyCollectedDataTypeTracking: false,
            NSPrivacyCollectedDataTypePurposes: [
              'NSPrivacyCollectedDataTypePurposeAppFunctionality',
            ],
          },
        ],
        NSPrivacyTracking: false,
      },
    },
  },
};
Enter fullscreen mode Exit fullscreen mode

Android — Google Play Data Safety

The form you complete in the Play Console's App Privacy tab. The Data safety form must match your privacy policy and your app's actual behaviour. A mismatch can delay release or create a compliance problem later.

This is where your PII_INVENTORY file pays off: you fill the form from the inventory rather than from memory.


10. Third-party SDK data flows

We covered the supply chain in M2 and SDK traffic in M5. The M6 question is: which personal data do these SDKs collect, and who do they send it to?

Don't rely only on package.json. React Native apps pull native SDKs through CocoaPods, Gradle, Expo config plugins, and transitive dependencies.

Checklist:

  • What data does the SDK collect? (check the traffic, not the docs)
  • Does it ship its own privacy manifest? (Apple requires it)
  • Does its behaviour change when ATT is denied?
  • Is the collected data declared in your Data Safety form?
  • Do you have a data processing agreement (DPA) in place?

11. User rights: access, export, deletion

GDPR and similar regulations give users the right to access their data and request deletion. That has to exist in the app.

// screens/PrivacySettingsScreen.jsx
export default function PrivacySettingsScreen() {
  return (
    <ScrollView>
      <Section title="Your data">
        <Row
          label="Download my data"
          description="Receive a copy of all data related to your account by email"
          onPress={requestDataExport}
        />
        <Row
          label="Delete my account"
          description="Your account and all your data are permanently deleted"
          onPress={confirmAccountDeletion}
          destructive
        />
      </Section>

      <Section title="Permissions">
        <Toggle
          label="Analytics"
          description="Helps us improve the app"
          value={consent.analytics}
          onChange={(v) => updateConsent('analytics', v)}
        />
        <Toggle
          label="Personalized ads"
          value={consent.advertising}
          onChange={(v) => updateConsent('advertising', v)}
        />
      </Section>
    </ScrollView>
  );
}
Enter fullscreen mode Exit fullscreen mode

Centralize consent management — scattered code toggling SDKs one by one becomes a violation the moment one toggle is forgotten:

// services/consent.js
const consentState = { analytics: false, advertising: false };

export async function applyConsent(newState) {
  Object.assign(consentState, newState);

  // ✅ Every SDK managed from one place
  analytics.setEnabled(consentState.analytics);
  adSdk.setPersonalizationEnabled(consentState.advertising);
  crashReporting.setEnabled(consentState.analytics);

  await persistConsent(consentState);
  await logConsentChange(consentState);  // ✅ Keep a consent record
}
Enter fullscreen mode Exit fullscreen mode

That last line matters: record the consent. In an audit, saying "the user consented" isn't enough — you need to be able to show it.


Testing Strategies

Privacy testing differs from the other items: you aren't looking for a flaw, you're looking for excess.

Scanning traffic for PII

# pii_scanner.py — a mitmproxy script
import re

PATTERNS = {
    'EMAIL': re.compile(rb'[\w.+-]+@[\w-]+\.[\w.]+'),
    'PHONE': re.compile(rb'\b\d{10,}\b'),
    'IBAN': re.compile(rb'\b[A-Z]{2}\d{2}[A-Z0-9]{10,30}\b'),
}

def request(flow):
    scan(flow.request.pretty_url.encode(), 'URL', flow)
    if flow.request.content:
        scan(flow.request.content, 'BODY', flow)

def scan(data, where, flow):
    for name, pattern in PATTERNS.items():
        if pattern.search(data):
            print(f"⚠️  {name} in {where}{flow.request.host}{flow.request.path[:60]}")
Enter fullscreen mode Exit fullscreen mode

Run this and navigate through the app. Every unexpected host and every match deserves investigation.

Log scanning

# Android — scan logs for PII while the app runs
adb logcat -c && adb logcat | grep -iE "email|token|password|@.*\.(com|net)|[0-9]{10,}"

# iOS — Console.app, or:
xcrun simctl spawn booted log stream --predicate 'processImagePath contains "YourApp"' \
  | grep -iE "email|token|password"
Enter fullscreen mode Exit fullscreen mode

Backup content check

# Android — take a backup and inspect its contents
adb backup -f backup.ab com.yourapp
dd if=backup.ab bs=1 skip=24 | zlib-flate -uncompress | tar -tvf -

# With allowBackup="false" this should come back empty
Enter fullscreen mode Exit fullscreen mode

Automated tests

// __tests__/privacy.test.js
import { redact } from '../src/utils/redact';

describe('redaction', () => {
  it('masks email addresses', () => {
    const input = { note: 'Contact jane@example.com for details' };
    expect(JSON.stringify(redact(input))).not.toContain('jane@example.com');
  });

  it('masks PII keys', () => {
    const input = { email: 'a@b.com', phone: '5551234567', name: 'Jane' };
    const out = redact(input);
    expect(out.email).toBe('[REDACTED]');
    expect(out.phone).toBe('[REDACTED]');
  });

  it('leaves non-PII fields alone', () => {
    const input = { screenName: 'Home', itemCount: 5 };
    expect(redact(input)).toEqual(input);
  });
});
Enter fullscreen mode Exit fullscreen mode

Manual test scenarios

Test Method Expected
Permission rationale Trigger every permission prompt Explanation appears first
Permission refusal Deny every permission App still works
ATT refusal Deny ATT, watch traffic No IDFA sent
Log leakage Log in, scan adb logcat No PII
URL PII Scan all traffic No PII in query strings
Backup Run adb backup Empty or non-sensitive only
App switcher Background from a sensitive screen Screen is masked
Account deletion Delete account, try logging back in Data actually gone
Declaration accuracy Data Safety form vs real traffic They match

Tools and Resources

Privacy testing tools

Tool Use Case
mitmproxy + custom script Scanning traffic for PII
MobSF Static analysis of permissions and data flows
Exodus Privacy Detects trackers in an APK
App Privacy Report (iOS 15+) Shows what each app accessed on-device
Privacy Dashboard (Android 12+) Permission usage history

React Native libraries

Library Purpose
react-native-permissions Unified permission management
react-native-tracking-transparency iOS ATT
babel-plugin-transform-remove-console Production log hygiene
react-native-screenguard Blocking screenshots/recording
@react-native-clipboard/clipboard Clipboard control

Useful resources

Resource URL
OWASP M6 owasp.org/www-project-mobile-top-10
User Privacy Protection Cheat Sheet cheatsheetseries.owasp.org
Testing User Privacy Protection (MASTG) mas.owasp.org
OWASP Top 10 Privacy Risks owasp.org/www-project-top-10-privacy-risks
Apple Privacy Manifest developer.apple.com
Expo Privacy Manifests docs.expo.dev/guides/apple-privacy
GDPR gdpr.eu

Conclusion

M6 is the item engineering teams defer most easily, because nothing breaks. Tests pass, the app runs, users don't complain. The bill arrives much later and much larger.

🎯 Security Roadmap

1. Beginner level

  • Build a PII inventory (what data, why, where to)
  • Strip console.log in production
  • Remove PII from URL query parameters
  • Set allowBackup and hasFragileUserData explicitly
  • Write a meaningful usage description for every permission

2. Intermediate level

  • Just-in-time permission requests + rationale screens
  • Crash reporting redaction layer (beforeSend)
  • Permission minimization (COARSE, photo picker, WHEN_IN_USE)
  • ATT implementation + actually honouring refusal
  • Fill Privacy Manifest and Data Safety from the inventory
  • Clipboard TTL and masking on sensitive screens

3. Enterprise level

  • Centralized consent management + consent audit log
  • User rights: data export and deletion flows
  • Retention periods and automatic deletion
  • PII traffic scanning in CI
  • DPAs and regular audits for third-party SDKs
  • Privacy-focused threat modeling

Key Takeaways

  1. Technical impact LOW, business impact SEVERE. This inversion explains both why M6 keeps getting deferred and why it shouldn't be.

  2. The best protection is not collecting the data at all. OWASP's six minimization questions come before adding encryption.

  3. "We don't collect PII" is almost never true. IP address, device ID, usage logs, and crash metadata are already PII.

  4. M6 is a lens applied to the other items. M5 + PII, M9 + PII, M3 + PII... but it has a question of its own: was this data ever necessary?

  5. Logs and crash reports are the most common leak point. Writing a beforeSend filter takes far less than a week and protects far more.

  6. Consent isn't done once you've asked. You have to actually turn things off for those who decline, and record the consent — that's the "ability to show" the lawsuit language refers to.

  7. Declarations must match reality. The Privacy Manifest and Data Safety form should reflect your app's real behaviour. A form filled from memory is a mismatch waiting to be found.

Next Steps

In our next article, we'll examine M7: Insufficient Binary Protections. We'll cover protecting the app binary against reverse engineering, tampering, and code injection, along with Hermes bytecode, obfuscation, and integrity checking strategies in React Native.


References


This article is the sixth in the OWASP Mobile Top 10 2024 series. Previous articles covered M1 through M5. Follow along for the rest of the series!

Top comments (0)