Edge AI Security Atlas

Threat Library deep dive for result ownership, tenant isolation, and object-level authorization on the Pi gateway.

A3 / Software attack / High

Broken Object Authorization

A user authenticates successfully, but the Pi gateway reads or modifies an inference result by client-supplied object ID without proving the object belongs to that user. In an Edge AI lab, this can leak uploaded-image metadata, predictions, backend routing decisions, latency records, and experiment results across users or projects.

Where It Appears In The Edge AI Architecture

A3 appears after authentication, when the gateway maps a result ID to stored inference state.

Authenticated client Pi result API Result store User A valid token User B owns R-2002 GET /results/{id} auth ok, object check? ID EXISTS ONLY R-1001 owner=user-a R-2002 owner=user-b
Cross-user object read A valid token reads a result object owned by a different user because the API only checks that the ID exists.

Threat Model

The attacker is authenticated but not authorized for the target object.

Attacker capability

The attacker has a legitimate account or lab token and can call result routes that are meant for normal users. They can observe their own result IDs, infer naming patterns, change path parameters, and compare status codes and response shapes.

Security assumption being tested

Authentication proves who the caller is. It does not prove that the caller can read, delete, compare, export, or rerun a specific result object. A3 tests whether every object lookup is scoped by user, project, tenant, role, and action.

Assets at risk

Uploaded image metadata, prediction labels, confidence scores, backend choice, latency records, model version, experiment notes, admin logs, and saved benchmark comparisons between Jetson and Zynq.

Out of scope

No real account probing, no third-party API testing, no credential abuse, and no access attempts against systems you do not own. The included code uses two toy users and synthetic local result records.

Attack Intuition

A3 begins where A1 ends: the caller may be logged in, but object ownership is still unproven.

A user submits an image and receives a result such as R-1001. The browser later calls GET /api/v1/results/R-1001. If the server simply loads whichever object ID appears in the URL, changing the ID to R-2002 may return another user's inference record. This is the classic broken object-level authorization pattern applied to ML result storage.

The bug is easy to miss because the route already has authentication. Unit tests may prove that anonymous requests are blocked, while never proving that a logged-in user is constrained to their own objects. In research portals, that mistake can leak not only predictions but experimental metadata: which backend ran, which model hash was used, how long the accelerator took, and whether a sample is part of a sensitive dataset.

Safe framing: the demonstration below is a localhost toy service with hard-coded users and synthetic records. It exists to teach the defensive check, not to test external APIs.

Technical Explanation

Object authorization must be part of the query and the policy decision, not an afterthought.

Vulnerable sequence

  1. Gateway validates the bearer token and extracts user-a.
  2. Route receives result_id=R-2002 from the client path.
  3. Database query loads R-2002 by ID alone and returns it.

Secure sequence

  1. Gateway validates token, role, tenant, and project context.
  2. Object query is scoped by result_id and owner_id or equivalent policy.
  3. Cross-user reads return 403 or indistinguishable 404, with safe audit logging.

Edge AI-specific detail

The object may not be just a row. It can reference an image blob, feature vector, model version, backend route, Zynq timing trace, or Jetson TensorRT engine metadata. Ownership must cover the whole object graph.

A robust implementation avoids trusting client-supplied IDs as the security boundary. Use opaque IDs for enumeration resistance, but do not confuse unpredictability with authorization. The real control is policy: subject, action, object, owner, tenant, and purpose.

Mathematical Formulation

A3 is a missing predicate in the authorization relation.

Subject s = authenticated caller Object o = inference result, uploaded image, benchmark run, or log bundle Action a = read, delete, export, compare, rerun Allow_vulnerable(s, a, o) = Authenticated(s) * ObjectExists(o) Allow_secure(s, a, o) = Authenticated(s) * ObjectExists(o) * Permitted(policy, s, a, o) * SameTenant(s, o) * PurposeAllowed(s, a, o) Information leaked by one object: I(o) = H(prediction, confidence, model_version, backend, latency, input_metadata) Expected leakage from ID probing: E[leakage] = sum_o P(guess o) * Allow_vulnerable(s, read, o) * I(o)

The secure check is not "does this ID exist?" It is "does this subject have this action on this object under the current policy?" The experimental goal is to prove that changing the ID changes only the authorization decision, not the user's effective access.

Step-By-Step Safe Lab Demonstration

The lab contrasts authenticated ID-only access with authenticated owner-scoped access.

  1. Save the Python code from the next section as a3_object_authorization_lab.py.
  2. Start vulnerable mode with python3 a3_object_authorization_lab.py --mode vulnerable.
  3. Request R-1001 using User A's token. This should succeed because User A owns it.
  4. Request R-2002 using User A's token. Vulnerable mode incorrectly returns User B's synthetic result.
  5. Restart with python3 a3_object_authorization_lab.py --mode secure.
  6. Repeat the cross-user request. Secure mode returns 403 and logs a safe object-authorization denial.

Interactive toy replay

Replay the expected local behavior without starting the Python server. The output is illustrative and sends no requests.

ready: no object-check simulation replayed yet

Full Code For A Local Simulated Lab

The server binds to 127.0.0.1 and uses toy tokens, users, and synthetic result objects only.

a3_object_authorization_lab.py
#!/usr/bin/env python3
"""
A3 local-only simulator: broken object authorization.

This toy API binds to 127.0.0.1 and uses synthetic result objects.
It demonstrates why authentication is not enough: every object lookup
must also enforce ownership or an equivalent authorization policy.
"""

import argparse
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse

TOKENS = {
    "Bearer token-user-a": {"user_id": "user-a", "role": "researcher", "tenant": "edge-lab"},
    "Bearer token-user-b": {"user_id": "user-b", "role": "researcher", "tenant": "edge-lab"},
    "Bearer token-admin": {"user_id": "admin-1", "role": "admin", "tenant": "edge-lab"},
}

RESULTS = {
    "R-1001": {
        "owner_id": "user-a",
        "tenant": "edge-lab",
        "label": "synthetic-cat",
        "confidence": 0.934,
        "backend": "jetson",
        "latency_ms": 47,
        "model_hash": "onnx-demo-7ad3",
    },
    "R-2002": {
        "owner_id": "user-b",
        "tenant": "edge-lab",
        "label": "synthetic-dog",
        "confidence": 0.881,
        "backend": "zynq",
        "latency_ms": 38,
        "model_hash": "bit-demo-19fe",
    },
}


def can_read_result(principal, result):
    if principal["role"] == "admin" and principal["tenant"] == result["tenant"]:
        return True
    return principal["user_id"] == result["owner_id"] and principal["tenant"] == result["tenant"]


class ResultHandler(BaseHTTPRequestHandler):
    server_version = "A3LocalGateway/1.0"

    def log_message(self, fmt, *args):
        print("%s - %s" % (self.log_date_time_string(), fmt % args))

    def _send_json(self, status, document):
        body = json.dumps(document, indent=2).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _principal(self):
        token = self.headers.get("Authorization", "")
        return TOKENS.get(token)

    def do_GET(self):
        parsed = urlparse(self.path)
        parts = [part for part in parsed.path.split("/") if part]
        if len(parts) != 4 or parts[:3] != ["api", "v1", "results"]:
            self._send_json(404, {"error": "not found"})
            return

        principal = self._principal()
        if not principal:
            self._send_json(401, {"error": "missing or invalid bearer token"})
            print("event=reject reason=missing_auth route=/api/v1/results")
            return

        result_id = parts[3]
        result = RESULTS.get(result_id)
        if not result:
            self._send_json(404, {"error": "result not found"})
            print("event=object_miss user=%s result_id=%s" % (principal["user_id"], result_id))
            return

        if self.server.mode == "secure" and not can_read_result(principal, result):
            self._send_json(403, {"error": "object access denied"})
            print(
                "event=object_denied user=%s result_id=%s owner=%s action=read"
                % (principal["user_id"], result_id, result["owner_id"])
            )
            return

        response = {"result_id": result_id, "result": result}
        self._send_json(200, response)
        print(
            "event=object_read mode=%s user=%s result_id=%s owner=%s backend=%s"
            % (self.server.mode, principal["user_id"], result_id, result["owner_id"], result["backend"])
        )


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--mode", choices=["vulnerable", "secure"], default="vulnerable")
    parser.add_argument("--port", type=int, default=8081)
    args = parser.parse_args()

    server = ThreadingHTTPServer(("127.0.0.1", args.port), ResultHandler)
    server.mode = args.mode
    print("A3 local lab listening on http://127.0.0.1:%s mode=%s" % (args.port, args.mode))
    print("Toy tokens: Bearer token-user-a, Bearer token-user-b, Bearer token-admin")
    server.serve_forever()


if __name__ == "__main__":
    main()
localhost client checks
# Terminal 1: intentionally vulnerable toy service
python3 a3_object_authorization_lab.py --mode vulnerable

# User A reads their own result.
curl -s http://127.0.0.1:8081/api/v1/results/R-1001 \
  -H "Authorization: Bearer token-user-a"

# User A reads User B's result in vulnerable mode. This is the local-only A3 demonstration.
curl -i -s http://127.0.0.1:8081/api/v1/results/R-2002 \
  -H "Authorization: Bearer token-user-a"

# Terminal 1: secure toy service
python3 a3_object_authorization_lab.py --mode secure

# User A still reads their own result.
curl -s http://127.0.0.1:8081/api/v1/results/R-1001 \
  -H "Authorization: Bearer token-user-a"

# Cross-user read is blocked by object ownership policy.
curl -i -s http://127.0.0.1:8081/api/v1/results/R-2002 \
  -H "Authorization: Bearer token-user-a"

Practical Example For Your Edge AI + FPGA + Jetson Setup

A3 protects result records produced by both private backends.

Expected topology

The Pi gateway creates a result object after dispatching inference to Jetson or Zynq. The object may contain prediction output, backend name, latency, model hash, input digest, and experiment tag. That object should be attached to a user, project, tenant, and retention policy.

Pi result API Jetson result Zynq result

A3 failure mode

User A submits to Jetson and sees R-1001. User B submits to Zynq and gets R-2002. If User A can fetch R-2002, the Pi leaks another user's prediction and backend metadata without any direct backend exposure.

IDOR cross-user result D1 missing object check

In your portal, the object graph matters. A result object can point to stored images, trace bundles, model versions, and accelerator benchmark records. Secure design should authorize each object family consistently, not only the top-level result row.

Observable Signals Or Logs

A3 should be visible as object-scope denies, not backend events.

SignalVulnerable observationHardened observation
HTTP status200 when user changes result ID to another user's object403 or policy-shaped 404 for cross-user object access
Gateway logevent=object_read user=user-a result_id=R-2002 owner=user-bevent=object_denied user=user-a result_id=R-2002 action=read
Backend logNo new inference needed; data leaks from result storeNo backend call; denial occurs at Pi result API
Database querySELECT by result_id onlySELECT scoped by result_id plus owner, tenant, project, or policy join
Detection metricRepeated object misses or cross-owner reads may blend into normal 200sObject-denied counter, user/object mismatch rate, and suspicious ID-walk patterns

Impact Analysis

A3 is mostly a confidentiality and integrity issue, with research-process consequences.

Confidentiality

Cross-user reads expose predictions, confidence scores, sample metadata, backend routing, model hashes, and possibly links to uploaded images or trace bundles.

Integrity

If write, delete, rerun, export, or label-update routes share the same flaw, one user can manipulate another user's experimental records or training feedback.

Availability

Availability is secondary, but unauthorized delete or rerun operations can remove results, trigger extra backend work, or corrupt experiment queues.

Mapping To CIA, STRIDE, PASTA, And MITRE ATLAS

A3 is an API authorization problem that can expose AI model behavior and data.

FrameworkA3 mappingResearch interpretation
CIAConfidentiality and Integrity primary; Availability secondaryProtect result objects and related artifacts from cross-user read, write, delete, export, and rerun actions.
STRIDEElevation of Privilege, Information Disclosure, Tampering, RepudiationA normal user gains object-level access beyond their policy and may leave insufficient audit evidence.
PASTAStage 3 decomposition, Stage 4 threat analysis, Stage 5 vulnerability analysis, Stage 6 attack modelingDecompose result, image, log, model-version, and benchmark objects; test each action with cross-user fixtures.
OWASP API SecurityAPI1:2023 Broken Object Level AuthorizationEvery endpoint that receives an object ID and acts on a record needs object-level authorization, not only authentication.
MITRE ATLASRelevant to model exposure and exfiltration-through-inference concerns, including AML.T0024 Exfiltration via ML Inference API when result access exposes model behavior or inference outputs.A3 can leak AI outputs and model metadata from stored objects rather than live inference alone.

Defense Mapping To Existing D1-D11 Controls

D1 is the primary control because it includes both authentication and object checks.

ControlRole against A3Validation
D1 JWT Auth + Object ChecksPrimary control. Validate token, derive subject context, and enforce object ownership or policy on every result action.Cross-user reads, exports, deletes, reruns, and label updates return 403 or policy-shaped 404.
D6 Sanitized LoggingRecords object-denied events without storing tokens, request bodies, or sensitive images.Logs include user id, action, object id hash, owner mismatch, tenant, status, and trace id.
D5 Query Anomaly DetectionDetects ID walking, repeated object misses, and abnormal result-export patterns.Alert on high object-denied rates or many neighboring result IDs from one user.
D4 Private Backend SubnetDoes not fix A3 directly, but ensures result access remains mediated by the Pi gateway rather than backend side routes.Client VLAN cannot fetch backend-local result or benchmark files.

Research Notes: What To Measure Experimentally

A3 is well suited for automated cross-user authorization tests.

Object action matrix

For each object type, test read, list, export, delete, compare, rerun, and annotate. The expected result is own object allowed, cross-user object denied, admin scoped by policy, and unrelated tenant denied.

Query-level enforcement

Measure whether secure queries use owner or policy scope at the database layer, not just post-query filtering. Post-query checks can still leak timing and existence signals.

Enumeration resistance

Compare sequential IDs and opaque IDs. Opaque IDs reduce guessability, but the authorization test must still fail safely if a valid cross-user ID is known.

Audit quality

Measure object-denied counts, ID-walk patterns, cross-owner mismatch rates, and whether logs contain enough context for incident response without leaking payloads.

Key Takeaways

Authentication is a starting point, not an object-level access decision.

  • A3 occurs when the gateway loads objects by client-supplied ID without proving the caller may act on that object.
  • Result objects in Edge AI carry sensitive model behavior, backend routing, and experiment metadata, not just labels.
  • D1 must enforce object ownership or policy on every read, list, export, delete, rerun, and annotation route.
  • Automated tests should include cross-user fixtures for every object family in the portal.