Skip to content

ApiConfiguration under /admin is readable by any ROLE_USERconfigJson exposes upstream credentials #8

Description

@schmunk42

Problem

src/Entity/ApiConfiguration.php (master, unchanged as of 2026-08-23) restricts every write operation to ROLE_ADMIN, but leaves reads at ROLE_USER:

routePrefix: '/admin',
security: "is_granted('ROLE_USER')",

configJson is a plain readable property — no normalizationContext groups are declared on the resource, and ApiProperty defaults to readable: true:

#[ORM\Column(name: 'config_json', type: Types::JSON)]
#[JsonSchema]
#[Assert\NotBlank]
#[ApiConfigurationConstraint]
#[ApiProperty(description: 'API configuration object. Must match one of the supported API types.')]
private array $configJson = [];

configJson is exactly where credentials for the upstream system live — the field's own JSON schema models auth_type: basic|bearer with username/password or token. So any authenticated user can read the credentials of every configured upstream API via GET /api/admin/api_configurations.

In our case one configuration holds HTTP basic credentials for a workflow engine whose REST API can deploy processes and mutate process state, and another holds a bearer token for an upstream data service. Any user who can obtain a token for the application — including the lowest-privileged domain role — can read both and then talk to those systems directly, bypassing the application entirely.

This is not merely a matter of degree versus the Job-resource case in the sibling bundle: read access to a credential store is a privilege escalation, not an information leak.

Why this looks like an oversight

The write operations were deliberately narrowed to ROLE_ADMIN (Post, Put, Patch, Delete, and the …/authorize sub-resource), and the resource sits behind routePrefix: '/admin' with openapi: new Operation(tags: ['System']). Everything about the resource says "operations only" except the read default. The sibling bundles that expose operational resources under /admin (doctrine-audit-log-bundleLogEntry, symfony-system-resources-bundleDoctrineMigrationVersion / MessengerMessage) all default to ROLE_ADMIN.

Proposal

Both parts, not either/or:

  1. Resource default → ROLE_ADMIN (security: "is_granted('ROLE_ADMIN')"), matching the write operations and the sibling bundles. The …/health sub-resource can stay broader if consumers rely on it for status displays — it exposes no secrets.
  2. Never serialize secrets, regardless of the role. Even for an admin, returning password / token over the API is more than the resource needs. Options, roughly in order of preference:
    • redact known credential keys in a normalizer/output DTO (password, token, secret, *_secret) so the shape stays intact but values are masked;
    • or #[ApiProperty(security: "is_granted('ROLE_ADMIN')")] on configJson as a minimum step;
    • long term, keep secrets out of the row entirely (env/secret-store reference in configJson, resolved server-side). We already do this in one application for a client secret specifically because configJson is API-readable — it would be good if the bundle made that the default rather than something each consumer has to remember.

Impact

Breaking for consumers whose non-admin clients read the configuration list (e.g. to discover a base URL). Suggest a config option or a reduced read projection for that use case rather than keeping the current default.

Notes for the fix

  • Regression tests worth having: authenticated non-admin gets 403 on collection and item; an admin read does not contain the credential values.
  • Reproduction is straightforward: seed a configuration with auth_type: basic, then GET /api/admin/api_configurations with a token that carries only the baseline authenticated role.

Reported with AI assistance (Claude Code).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions