Your OData API will hand a stranger the entire shape of your database, no login required

OData is the query protocol behind a lot of .NET, Dynamics, SAP, and SharePoint back ends. It has one feature that makes it convenient to build against and risky to leave open: a single URL that describes the entire service. Hit /$metadata (or /odata/$metadata) and you get back an XML document listing every entity set the API exposes, every property on each one, the keys, the types, and the relationships between them. It is the map a client reads on startup so it knows what it can ask for.

That same map is a gift to anyone who isn't supposed to have it. You didn't have to guess your tables are called Customers, Orders, and Invoices, or that Customer has an Email and a TaxId, or that orders link to payments. The service told you, in one anonymous request, before you sent a single query.

What an exposed metadata document looks like

The document announces itself. It's wrapped in an EDMX envelope, it carries a CSDL schema, and it spells out the model in plain XML. No part of it is obfuscated, because clients are meant to read it.

request
GET /odata/$metadata HTTP/1.1 Host: yoursite.com
response
HTTP/1.1 200 OK
Content-Type: application/xml
<edmx:Edmx Version="4.0">
<Schema Namespace="Sales.Models">
<EntityType Name="Customer">
<Property Name="Email" .../>
<Property Name="TaxId" .../></EntityType>
<EntitySet Name="Customers" .../></Schema>
</edmx:Edmx>
The whole data model in one anonymous response: entity sets, property names, keys, relationships.

That clean 200 with an EDMX body is the signal. It means the request reached the service's model and the service answered without asking who's asking. From there an attacker knows precisely which collections to probe and which property names to filter on, which is half the work of finding the one endpoint that returns more than it should.

What this does and doesn't prove

Worth being straight about scope, because it's where this finding gets over-claimed. An exposed $metadata document proves the schema is public. It does not, on its own, prove the rows are. Plenty of OData services publish their model anonymously but still require a token to read actual records, and plenty of public read-only feeds publish their metadata on purpose because the whole API is meant to be open. So the finding says what it can actually see: the data model is readable without authentication. Whether the records behind it are also readable is a separate question that needs an authenticated review to answer, and the scanner doesn't guess at it.

That honesty is also why the check is calibrated low rather than dressed up as a critical leak. It's a recon exposure: real, worth closing, but not proof that your customer table is being downloaded right now.

Why it doesn't false-fire

The gate is tight on purpose. A 200 isn't enough, because a single-page app that answers every path with its shell would trip a lazy check. The finding requires the response to actually be an EDMX document: an XML content type, the EDMX envelope as the real document root rather than a fragment quoted in some docs page, a schema with a namespace, and at least one declared entity set or type. And it only fires on an anonymous request, so a service that answers /$metadata with a 401 or 403 is doing the right thing and stays quiet. The check reads the metadata and counts the entity sets for evidence; it never sends a $filter, $expand, or any query that would pull a record.

Closing it

The fix is to put the model behind the same authentication as the data, including the $metadata route, which frameworks sometimes leave open even when the rest of the API is gated.

# the framework scaffolded an OData controller and
# left it anonymous; /$metadata answers everyone
GET /odata/$metadata -> 200, full model
# -> the schema is public, queries may be too
Require auth on the whole service, $metadata included. If it's meant to be public, make sure no entity set over-returns.

Require authentication on the OData endpoint and confirm it covers $metadata, which lives at its own path and is easy to leave open. If the API genuinely needs to be public, go through the entity sets and make sure none of them returns sensitive records to an unauthenticated query. And if the endpoint was scaffolded by a template and you're not actually using it, the cleanest fix is to delete it. An OData controller you forgot you generated is the most common way this ends up on the open web.

Reading it from outside

Whether your OData model is readable without a login is something anyone can settle with one GET of /$metadata, which is exactly what SurfaceCheckr does: it fetches the metadata path, confirms the response really is an EDMX document, and reports the exposure only when the model comes back to an anonymous request, never on a 401, an HTML soft-404, or a plain XML feed that isn't OData. It counts the entity sets as evidence and reads no further, because the model is the finding and the records are not something a passive scan should touch. For the broader pattern of APIs that volunteer more than they should, an API leaking personal data in its responses is the next read, and GraphQL introspection left on in production is the same mistake in a different protocol.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.