---
title: Security and authorization
summary: Authorization enforcement points, exposure findings, and the anonymous /Ping request path.
status: draft
owner: SAIF Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.PingableService
reference_type: package
---

# Security and authorization

> **Scope note:** Activated (narrow scope). This library has one live authorization decision (`[AllowAnonymous]` on `PingableController.Get`) plus an exception-message exposure path and an unauthenticated workload. There are no AD resources, roles, policies, security managers, or data-level checks in this library. Authentication is out of scope.

## Authorization architecture

This library's authorization architecture is a single, local, attribute-based opt-out: `[AllowAnonymous]` on `PingableController.Get` exempts `/Ping` from whatever host-level authorization the consuming application has configured. There is no centralized security service, no AD-group check, no policy evaluation, and no data-level check anywhere in this library — authentication is out of scope entirely, and the library registers no authentication mechanism of its own.

```mermaid
flowchart LR
    Caller["Anonymous caller<br>GET /Ping?guid=..."] --> AuthZ["Host UseAuthorization /<br>global AuthorizeFilter"]
    AuthZ -->|"skipped by [AllowAnonymous]"| Get["PingableController.Get"]
    Get --> Svc["DI-registered<br>IPingableService targets"]
    Get --> Ctl["Pingable controllers<br>(own attributes not evaluated)"]
    Svc --> Fail{"Any failure?"}
    Ctl --> Fail
    Fail -->|"no"| Ok["200 body = guid"]
    Fail -->|"yes"| Ex["PingServiceException<br>Ping error: guid + ex.Message"]
    Ex --> EH["DynamicErrorHandler -><br>injected IErrorHandler"]
    EH --> Resp["500 text/plain body = ex.Message<br>+ error-ID line, headers<br>Error-ID, Server-Name"]
```

| Metric | Value |
| --- | --- |
| Authorization architecture type | None (anonymous by design) — the library makes a single authorization decision, exempting `/Ping` from all host authorization, and delegates nothing to a security service. |
| Authentication mechanism | Out of scope; none registered by this library. |
| Total AD resource names referenced | 0 |
| Authorization enforcement points | 1 attribute-based (`[AllowAnonymous]`, opt-out); 0 programmatic |
| Endpoints exposed | 1 (`GET /Ping`), anonymous |
| Endpoints requiring authorization | 0 |
| Security managers / interfaces | 0 custom (the only security type consumed is the framework `AllowAnonymousAttribute`) |
| WCF SecurityService integration | No |
| Data-level authorization checks | 0 |
| SOD rules | 0 |
| Machine authorization present | No |
| Exposure findings | 3 (SEC-01 Medium information disclosure; SEC-02 Low reflected input/log forging; SEC-03 Low–Medium unauthenticated workload)* |

## AD resource catalog

None found. This library references no AD group, role, policy, LDAP string, or `SAMAccountName`/`$`-suffix machine-account pattern anywhere in its code. The only security-related type it consumes is the framework `Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute`, which is an opt-out, not an AD resource.

## Authorization enforcement points

### Attribute-based authorization

#### AZ-01 — AllowAnonymous attribute on GET /Ping

| Field | Value |
| --- | --- |
| Controller / action | `PingableController` (`[Route("Ping")]`, `[ApiController]`) / `Get(string guid = null)` — `GET /Ping`, `GET /Ping?guid=<value>` |
| Attribute | `[AllowAnonymous]` (`Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute`) |
| Scope | Action |
| Role(s) / policy | None |
| Effect | Exempts the route from every host-level authorization requirement, including a global MVC `AuthorizeFilter` and an endpoint-routing fallback policy, per standard ASP.NET Core `[AllowAnonymous]` override semantics.* |
| Source | `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cs` |

`PingableController` carries no `[Authorize]` attribute and has no base controller other than `ControllerBase`. Its class summary states the intent explicitly: "provide all API's an Anonymous /Ping route."

Because the library injects this controller into every host that calls `WithPingableController()` (see [Service registration](service-registration.md)), the anonymous route is **inherited by every consuming API** built on `SAIF.NetCore.ServiceExtensions`'s `BaseApiStartup` (including `CoreAPIStartup` and `EdgeAPIStartup`).

No in-repo test exercises `/Ping` against an active authorization requirement. The test hosts named `SecureStartup` and `SecureStartupWithScopedAndSingleton` do **not** configure or exercise authorization; "Secure" refers to a secure *configuration client* (`EnableSecureEnvironmentClient<SecureStartup>()`), not an authorization API, so neither test corroborates that `[AllowAnonymous]` overrides an active authorization requirement. [BR-01](business-rules.md#br-01-anonymous-access-to-ping-endpoint) remains verified directly from the `[AllowAnonymous]` attribute itself.

### Programmatic and security-manager checks

None found. The library performs no `IsInRole`, `IsAuthorized`, `HasAccess`, `CheckAccess`, `CheckClaim`, `IAuthorizationService.AuthorizeAsync`, or `User`/`ClaimsPrincipal` inspection. No `ISecurityManager`, `SecurityManager`, or custom `AuthorizeAttribute` subclass is defined or consumed.

### Indirect authorization bypass by design

`/Ping` discovers every application-part type that is both `IPingableService` and `ControllerBase`, resolves it from DI, and calls `PingAsync` on it as a plain method call. This invocation does not pass through MVC action selection or filters, so any `[Authorize]` attribute placed on a pingable controller **is not evaluated** for its `Ping`/`OnPingSuccess`/`OnPingFailure`/`IsThrowablePingError` hooks — whatever those hooks do is executed on behalf of an anonymous caller. The only in-repo pingable controller (`SAIF.NetCore.Services.Messaging`'s `BackgroundConsumerServicesController`) carries no authorization attribute and uses the default no-op `Ping`, so no concrete bypass exists today; the risk applies to consumer-authored pingable controllers and services.

## Data-level authorization

None. This library performs no entity-ownership or scope checks of any kind.

## Segregation of duties

None. No SOD rules exist in this library.

## Security interfaces and contracts

| Interface / Class | Role | Consumers | Source |
| --- | --- | --- | --- |
| `Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute` | Framework attribute consumed to opt `/Ping` out of host-level authorization | `PingableController.Get` | Framework package (`Microsoft.AspNetCore.Authorization`) |

No custom `ISecurityManager`, `SecurityManager`, or security-contract interface is defined or consumed by this library.

## Exposure findings on the anonymous endpoint

These are not authorization enforcement points, but they define what an unauthenticated caller can obtain or cause through AZ-01.

### SEC-01 — Exception messages and server name returned to anonymous callers (information disclosure)

| Field | Value |
| --- | --- |
| Severity | Medium (impact depends on consumer exception content)* |

On any failure, `PingableController.Get` throws `PingServiceException(ex.Message, ex, guid)`, which builds its message as `"Ping error: {guid}\n{message}"`, sets `StatusCode = 500`, and inherits `ContentType = "text/plain"`. In hosts built on `BaseApiStartup`, `BaseApiStartup.ConfigureMiddleware` calls `app.ConfigureErrorHandling()`, which adds `SAIF.NetCore.ServiceExtensions`'s `DynamicErrorHandler` (`IMiddleware`) to the pipeline — this is the current, host-integrated handler, distinct from the legacy `SAIF.NetCore.ErrorHandler`'s own `ErrorHandler.Invoke` middleware method, which that class keeps only for backwards compatibility and is not used by `BaseApiStartup`. `DynamicErrorHandler` catches the `HttpStatusCodeException`, runs any registered `IErrorHandlerHook`s (aggregating any hook failure together with the original exception rather than suppressing it), and then delegates the actual response formatting to its injected `IErrorHandler` (the `ErrorHandler` class), calling `ErrorHandler.LogAndReturnException`. That method first checks `context.Response.HasStarted`; if the response has already started, it logs a Warning and re-throws without writing a body, skipping everything below. Otherwise it logs the error at Error level and captures the `ErrorID` *before* touching the response, then clears the response, adds `Error-ID` and `Server-Name` (`Environment.MachineName`) response headers, sets status 500 and `text/plain`, and writes a body built by `ErrorHandler.BuildHandledResponseMessage`: `"{ex.Message}\nPlease see error: {ErrorID}."`.
> **Origin**: The hook-invocation/aggregation step above is implemented in [Dynamic error handler invocation](../saif-netcore-serviceextensions/operations/dynamic-error-handler-invoke.md) (`SAIF.NetCore.ServiceExtensions`); the already-started-response rejection and log-before-response sequence are implemented in [Log an exception and write the HTTP error response](../saif-netcore-errorhandler/operations/log-and-return-exception.md) (`SAIF.NetCore.ErrorHandler`). Both are executed on behalf of this library's failure path ([OP-05](operations/op-05-raise-ping-failure.md)).

Result: an anonymous caller of `GET /Ping` receives, in the 500 body, `Ping error: <guid>` followed by the **raw `Message` of the underlying exception**, plus the host machine name in the `Server-Name` header. `BuildHandledResponseMessage` does not serialize or traverse `InnerException`, stack traces, or `LogDetails` — only `ex.Message` plus the appended error-ID line is written.

What that message can contain (inferred): DI resolution failures ([BR-05](business-rules.md#br-05-resolution-and-construction-failures-count-as-ping-failures)) carry the framework message naming the unresolved service type and the type being activated, disclosing internal type and namespace names.* Consumer `Ping` overrides that test connectivity (database, message broker, downstream API) can surface driver messages such as host names, ports, database names, or login names.* For a faulted `Task.WhenAll`, the message is `AggregateException.Message`, which concatenates every failing target's own message together — this is message composition, not `InnerException` serialization, since `BuildHandledResponseMessage` never reads `InnerException` directly.

Without `DynamicErrorHandler` in the pipeline, the exception is unhandled; ASP.NET Core then returns a 500 without the SAIF-formatted message body outside the Developer Exception Page.*

### SEC-02 — Caller-controlled guid reflected into response bodies and logs

| Field | Value |
| --- | --- |
| Severity | Low |

The optional `guid` query parameter is not validated as a GUID; any string is accepted. It is (a) returned verbatim as the 200 body via `Ok(guid)`, (b) embedded in the 500 message built as `"Ping error: {guid}\n{message}"`, (c) passed to every `Ping`/hook implementation along with `ISaifLogService`, and (d) logged by `ErrorHandler` through `_logger.Error(ex, ex.Message)`. The 500 response is `text/plain`, which limits reflected-script risk in browsers.* The residual risk is log forging (embedded newlines in `guid` written to logs) and arbitrary-length input reflected in responses.*

### SEC-03 — Unauthenticated, uncached, fan-out workload (availability)

| Field | Value |
| --- | --- |
| Severity | Low to Medium (impact depends on the cost of consumer `Ping` implementations)* |

Each anonymous request resolves every `IPingableService` registration from DI; calls `Assembly.Load` and `GetTypes()` for every snapshotted application part with no caching ([OP-04](operations/op-04-enable-route-and-discover-controllers.md)); resolves every pingable controller from DI; and schedules one `Task.Run` per target on the thread pool, all concurrently ([OP-02](operations/op-02-default-ping-pipeline.md)). Consumer `Ping` overrides that open database or broker connections are executed per request.* The library applies no rate limiting, caching of results, or concurrency limit.

## Open questions / unknowns
- Consumer pingable implementations outside this repository define what `/Ping` actually executes anonymously and what exception text it can leak.**
- Whether `/Ping` is restricted at the gateway, ingress, or IIS/Kestrel binding level cannot be determined from this repository.**
- `SAIF.NetCore.Services.Messaging` pins an older package version whose controller source is not in this repository; whether it also carried `[AllowAnonymous]` is assumed but not verifiable here.**
- No regression test configures an authorization requirement and asserts that `/Ping` stays reachable, nor does any test assert the 500 response body/header content.**

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [Service registration](service-registration.md)
- [API reference](api/index.md)
- [Dynamic error handler invocation](../saif-netcore-serviceextensions/operations/dynamic-error-handler-invoke.md) (`SAIF.NetCore.ServiceExtensions`)
- [Log an exception and write the HTTP error response](../saif-netcore-errorhandler/operations/log-and-return-exception.md) (`SAIF.NetCore.ErrorHandler`)

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts. Statements marked with `**` could not be confirmed; treat
> as unknown until verified.

## Sources
- `src/SAIF.NetCore.PingableService/README.md`
- `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cs`
- `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Exceptions/PingServiceException.cs`
- `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/IPingableService.cs`
- `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Extensions/MvcBuilderExtensions.cs`
- `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Extensions/IServiceCollectionExtensions.cs`
- `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Services/ControllerRetriever.cs`
- `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Services/IControllerRetriever.cs`
- `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService.Tests/PingableControllerTests.cs`
- `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService.Tests/StartupTests.cs`
- `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService.Tests/Startup/SecureStartup.cs`
- `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService.Tests/Startup/SecureStartupWithScopedAndSingleton.cs`
- `src/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService.Tests/Startup/StartupWithPingableController.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IAppBuilderExtensions.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/BaseApiStartup.cs`
- `src/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cs`
- `src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/HttpStatusCodeException.cs`
- `src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIFException.cs`
