Security research

Last updated: 27 August 2026

Scope and authorization. The vulnerability findings on this page come from testing against infrastructure and applications that I own and operate. The artifact-analysis section examines publicly distributed files offline, in an isolated environment. No third-party system was accessed, probed or attacked to produce any of it.

About

Independent security researcher and software developer. I am the sole developer and operator of MIMO, a two-sided field-service platform hosted at iridescent.gg, and I conduct authorized security testing against it as part of building and running it. My work covers application security testing, authorization and multi-tenant isolation analysis, vulnerability analysis, and network and server security testing.

MIMO is a live production system: a React Native and Expo application compiled to Android, iOS and web, backed by PostgreSQL with row-level security, twelve serverless functions, realtime subscriptions, and card payments moving between independent parties. It handles customer personal data, home addresses, service-location coordinates, and money moving between strangers. That combination is what makes the authorization boundaries below worth writing up.

Rules of engagement

Two kinds of work appear here, and they have different boundaries.

Selected findings

1. Trigger-based column guards silently fail under SECURITY DEFINER privilege context

Context. A service request is a negotiation between two parties. The customer owns the request details; the provider owns the counter-offer fields. Both parties hold a valid session and both sets of fields live on the same row, so row-level security alone cannot separate them at column granularity. A trigger was added to enforce which side may write which columns.

Finding. Written as SECURITY DEFINER, that trigger cannot distinguish a trusted call from an untrusted one. Inside a SECURITY DEFINER context the session role resolves to the function owner regardless of who initiated the write, so a direct data-API update from an authenticated client is indistinguishable from a call made through a sanctioned procedure. The guard evaluates identically in both cases, and therefore enforces nothing that the untrusted path cannot satisfy.

Impact. A customer holding an ordinary session could bypass the application layer, write directly to the table, and modify provider-owned negotiation fields including the proposed price and the proposed start time. The interface never exposes those fields to a customer, so the weakness is reachable only by calling the data API directly — which is the path an attacker takes anyway.

Resolution. The guard was changed to SECURITY INVOKER. Under invoker rights the session role is the client role for a direct API write and the owner role when reached through a trusted procedure, which makes the two paths distinguishable. The guard now short-circuits for trusted callers and enforces column ownership for everything else. Reassignment of the owning business, the requestor, or the linked job is rejected unconditionally on both paths.

Generalisation. Any trigger intended to tell a trusted writer from an untrusted one must run as invoker. This is an easily missed class: definer rights are the reflexive choice for privileged database code, and choosing them here inverts the guard from enforcing to permitting without producing an error, a failing test, or any other observable signal.

2. Realtime subscription reports success while authorised as anonymous

Context. Notification delivery uses a realtime subscription filtered to the recipient, with row-level security as the authorization boundary.

Finding. The realtime transport holds its own credential, separate from the session held by the data client. If a subscription is opened before that credential is set, the socket authenticates anonymously. The channel handshake still completes and the client still receives a subscribed acknowledgement, but every row is then dropped by row-level security before delivery. Applying the credential after the subscribe frame has been sent does not repair the existing subscription.

Impact. Silent and total loss of notification delivery, with a success signal at the client. The behaviour is fail-closed rather than fail-open, so this is an availability and integrity-of-signal problem rather than a disclosure one. It is written up because the diagnostic surface actively misleads: a subscription acknowledgement is not evidence of authorization, and treating it as such sends an investigation toward the message pipeline instead of the credential.

Resolution. The session token is now resolved and applied to the realtime transport, and awaited, before any channel is opened.

Diagnostic note. The authoritative check is to ask the server what the connection actually registered as, by reading the live subscription state for its applied filters and claims role. Inferring the signed-in identity from the most recent authentication timestamp is unreliable whenever several test accounts authenticate close together, and it produces a confident wrong answer that looks exactly like a broken pipeline.

3. Provider home address recoverable by trilateration from discovery results

Context. Customer discovery returns nearby providers with a distance. Many providers are sole operators whose registered base location is their home address.

Finding. A distance returned at full precision describes a circle of known radius around the provider. An unprivileged authenticated client chooses its own query coordinates, so it can repeat the query from arbitrary points and intersect the results. Three queries are enough to recover the base coordinate to within the precision returned, turning a proximity feature into an address-disclosure primitive.

Impact. Disclosure of a provider home address to any authenticated customer account. For a platform whose providers travel to strangers, this is the most consequential class of disclosure in the system.

Resolution. Three layered controls. Base coordinates are excluded from the public projection entirely and are never returned by any customer-reachable procedure. Distance is computed server-side, so a client cannot substitute its own claimed position for the comparison. The returned distance is quantised into fixed buckets before it leaves the database, which widens each intersection from a point into an area, so repeated sampling converges on a region rather than an address. Customers see a service-area label and a bucketed distance.

Generalisation. Any feature returning a precise measurement relative to a hidden point is a disclosure primitive when the querying party controls its own reference point. Quantisation belongs at the trust boundary, not in the presentation layer.

4. Account deletion designed without an object identifier

Context. Permanent account deletion is reachable from the application, and is required to be reachable by platform policy.

Design decision. The conventional shape for this endpoint accepts an account identifier and checks it against the caller. That check becomes the single point of failure for the most destructive operation in the system, and it is precisely the shape of an insecure direct object reference. Rather than implement the check carefully, the parameter was removed: the endpoint accepts no identifier of any kind. The subject is derived exclusively from the verified token on the request, so a request can only ever delete its own sender. There is no input through which a different account can be named.

Verification. Probed for: unauthenticated request rejected with 401; a publishable key carrying no user context refused; a structurally invalid token rejected at the gateway before reaching application code; non-POST methods rejected with 405.

Related. Cross-service cleanup that cannot join the database transaction — stored object deletion in particular — is recorded in a durable row before the identity is destroyed, so a partial failure is discoverable and replayable rather than leaving orphaned personal data with no record that it exists.

5. Payment authority held entirely server-side

Context. Customers pay providers directly through connected accounts. The client is a phone or a browser, and is fully attacker-controlled.

Design. The client transmits a job identifier and nothing else. Amount, currency, destination account and any fee are resolved server-side under a row lock, in one transaction, at the moment payment is opened. No monetary value, account identifier, price identifier or payment state is accepted from a client on any path. Money is stored as integer minor units throughout; no floating-point representation of currency exists in the schema.

Settlement integrity. A job is marked paid only by a provider webhook whose signature is verified over the exact unparsed request body. Each event identifier is claimed once before its effects are applied, so replaying a captured event is inert. Providers can set an agreed price but have no path to mark money as received; that transition is reachable only from a verified settlement event.

Reconciliation. A scheduled job repairs state after missed or delayed events. It is read-only against the payment provider, deriving each transition from a freshly retrieved authoritative object and applying it through the same narrow procedures the webhook path uses — so the recovery path cannot express a transition the primary path forbids.

6. Account enumeration through password reset response

Finding. A reset endpoint that distinguishes a known address from an unknown one is an account-existence oracle, and an unauthenticated one.

Resolution. The response is identical in both cases, and states that instructions have been sent if an account exists. The cost is a marginally worse experience for someone who mistyped their address; the benefit is that the endpoint cannot be used to test address lists.

7. Open redirect surface in the authentication callback

Finding. Authentication callbacks conventionally accept a post-action destination as a URL parameter. Honouring an attacker-supplied destination on a page reached immediately after authentication is an open redirect at the point of maximum user trust, and a ready-made phishing primitive.

Resolution. The callback ignores any destination parameter and routes only to fixed internal paths. Separately, the recovery token is handed to the password-setting page through same-origin session storage rather than the URL, so it does not enter browser history, referrer headers or server access logs, and it is cleared once consumed.

8. Triaging static-analysis warnings without weakening the boundary they flag

Context. The database linter reports seven warnings on this schema.

Analysis. Six are the same rule, flagging privileged functions callable by authenticated users. Each of those six exists specifically to cross an authorization boundary the caller cannot cross, and each performs its own check in place of the boundary it bypasses. Verified individually: every one is granted to authenticated and service roles only, never to the anonymous or public role, and each carries a caller-identity or membership check appropriate to what it exposes. The public provider projection is additionally gated on the discoverability flag and, per the trilateration finding above, deliberately omits base coordinates.

Conclusion. Six are expected, and are documented as such per function. Remediating them as the linter suggests — revoking execution, or converting them to invoker rights — would break the authorization crossings they exist to perform. The seventh, absence of breached-password rejection, is a genuine gap and is tracked under open items.

Generalisation. A privileged-function warning is a prompt to verify the compensating control, not a defect in itself. Suppressing the finding and applying the suggested fix are both wrong; the correct output is a per-function record of what the compensating control is.

9. A security control that would have denied service to every native client

Finding. The authentication provider offers a bot-mitigation control presented as a general hardening toggle. Its token is produced by a browser-based challenge, and it has no equivalent on a native mobile client. Enabling it would have caused every native sign-in, registration and password reset to fail, because those clients cannot produce the required token.

Conclusion. The control remains disabled, and is documented as deliberately disabled with the reason — so it is not switched on later by someone reading the provider dashboard and seeing a security setting turned off. If it is wanted, it has to be scoped to the web registration path alone.

Generalisation. A control whose failure mode is total authentication denial deserves the same change-control scrutiny as a change to the authorization model. Availability is a security property.

10. Authorization guard removed a screen from reachability

Finding. The navigation guard classified route segments against a hand-maintained list and redirected anything unrecognised. Several legitimate segments were absent, so opening those screens redirected the user away before the screen could render. Affected surfaces included provider discovery settings and, on the customer side, provider profiles and request details.

Impact. Availability only, with no authorization consequence. It is included for its failure mode and its test gap rather than its severity.

Resolution and test gap. Segments are now classified explicitly by experience, with unrecognised segments still redirecting, which is the correct default. The existing unit tests passed throughout, because they asserted the contents of the same list the implementation read: both sides of the test agreed with each other and disagreed with the application. The replacement enumerates the route directory on disk and fails, naming the segment, when a real route is unclassified or a classified segment no longer exists.

Generalisation. A test that reads the same declaration as the code under test verifies internal consistency, not correctness. Where a guard is driven by an inventory, the test has to rebuild that inventory from the underlying source of truth.

Open items

Disclosed because neither is exploitable for data access, and both are observable from outside anyway.

Third-party artifact analysis

Separately from the platform work above, I analyse compiled Java artifacts before they are trusted — specifically Minecraft server modifications, which I both write and review.

The threat model is a supply-chain one and it is not hypothetical. Mods ship as compiled JARs, are installed with full JVM privileges, and run on servers hosting other people's accounts and data. The ecosystem has already had at least one self-propagating compromise distributed through mainstream mod-hosting platforms, which infected build artifacts and spread to anything it could reach on the host. Provenance is therefore not a trust signal: "it was on a well-known mod site" describes the delivery mechanism of that incident. Before I run a mod on a server, or recommend one to somebody else who will, I check what it actually does.

Method

Worked example: an obfuscated 523-class server mod

Subject. A NeoForge server-side roles and permissions modification, 523 classes including 175 mixins, distributed as a compiled JAR. Named privately rather than here, since the analysis found nothing to disclose.

Obfuscation encountered. Three layers, in increasing order of how much they cost an analyst. Identifier mangling, with single and double-letter class names sitting alongside real ones. Semantic decoy naming, where the class named for achievement data actually implements the encrypted state store and the class named for cosmetic text accents implements permission-group resolution — misleading names cost more than absent ones, because they invite you to skip the file. And string-array obfuscation, with every literal moved into a static array built in the class initialiser and referenced by index, so the decompiler reported that it could not fully structure essentially every static initialiser and the source read as an array lookup with no indication of the value.

Recovery. Reconstructing those arrays recovered 2,490 string literals mapped back to their owning classes, which turns opaque indexed lookups into readable behaviour and makes the dangerous-API sweep meaningful rather than decorative.

Result. No process execution, no dynamic class loading, and no outbound network of any kind. The single runtime call reads free memory. All three uses of the URL type are local resource resolution — protection-domain self-location and classloader resource-presence checks — not remote fetches. The one socket-address reference is the signature of the login hook being mixed into, not a socket. The encrypted store is genuine AES-256-GCM protecting the mod's own state at rest, keyed from a system property, an environment variable, or a generated server-local key file.

Conclusion. Obfuscated to protect commercial logic, not to conceal malicious behaviour. Cleared for use.

On negative results. A clean finding is the normal and expected outcome, and it is the point of the exercise. The output is not a vulnerability; it is a defensible reason to trust an artifact that arrived with none. Analysis that only counts as worthwhile when it finds something will, eventually, find something that is not there.

Methodology

Coordinated disclosure

If you have found a vulnerability in MIMO or in iridescent.gg, please report it to security@iridescent.gg rather than through the general contact form, which is not an appropriate channel for exploit detail. Please include enough information to reproduce the issue, and allow reasonable time for a fix before public disclosure. Testing that degrades service for other users, accesses data belonging to another account, or modifies data you do not own is not authorized.

Contact