Security and authorization¶
Scope note: Activated (narrow scope). This library has one live authorization decision (
[AllowAnonymous]onPingableController.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.
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), 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 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 IErrorHandlerHooks (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); 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). Both are executed on behalf of this library's failure path (OP-05).
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) 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); resolves every pingable controller from DI; and schedules one Task.Run per target on the thread pool, all concurrently (OP-02). 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
/Pingactually executes anonymously and what exception text it can leak.** - Whether
/Pingis restricted at the gateway, ingress, or IIS/Kestrel binding level cannot be determined from this repository.** SAIF.NetCore.Services.Messagingpins 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
/Pingstays reachable, nor does any test assert the 500 response body/header content.**
Related documentation¶
- Index
- Overview
- Architecture
- Dependencies
- Operations and processes
- Business rules
- Service registration
- API reference
- Dynamic error handler invocation (
SAIF.NetCore.ServiceExtensions) - Log an exception and write the HTTP error response (
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.mdsrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Controllers/PingableController.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Exceptions/PingServiceException.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/IPingableService.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Extensions/MvcBuilderExtensions.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Extensions/IServiceCollectionExtensions.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Services/ControllerRetriever.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService/Services/IControllerRetriever.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService.Tests/PingableControllerTests.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService.Tests/StartupTests.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService.Tests/Startup/SecureStartup.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService.Tests/Startup/SecureStartupWithScopedAndSingleton.cssrc/SAIF.NetCore.PingableService/SAIF.NetCore.PingableService.Tests/Startup/StartupWithPingableController.cssrc/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/DynamicErrorHandler.cssrc/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IAppBuilderExtensions.cssrc/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/BaseApiStartup.cssrc/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/SAIF.NetCore.ErrorHandler/ErrorHandler.cssrc/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/HttpStatusCodeException.cssrc/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIFException.cs