dotCMS / dotCMS/core

POST /api/v1/apps returns 500 with a raw NPE when a secret's value is missing, instead of 400

Open
#37,057 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

dotCMS : Rest API Team : Maintenance Type : Defect
Dominant language
Java
Stars
970
Forks
486
Avg merge
3d 33m
Merged PRs (30d)
170

Description

Description

POST /api/v1/apps/{key}/{siteId} returns HTTP 500 with a raw Java NPE message when a request body omits a secret's value, instead of a 400 with a useful validation error.

Input.InputDeserialize dereferences the value node without checking it, while null-guarding hidden on the very next line:

// dotCMS/src/main/java/com/dotcms/rest/api/v1/apps/Input.java:60-65
final JsonNode jsonNode = jsonParser.readValueAsTree();
final JsonNode value  = jsonNode.get("value");
final JsonNode hidden = jsonNode.get("hidden");
return newInputParam(value.asText().trim().toCharArray(),   // <-- NPE when "value" is absent
        hidden != null && hidden.asBoolean());              // <-- correctly guarded

The asymmetry in adjacent lines suggests an oversight rather than a decision. Input even declares @NotNull on the field, but bean validation cannot help: the NPE happens during deserialization, before SecretForm.checkValid() ever runs.

Steps to Reproduce
SITE=<a site identifier>
AUTH=$(printf 'admin@dotcms.com:admin' | base64)

# Any body where an Input object has no "value" key. Here the caller wrapped the map in
# the field name, so Jackson maps "inputParams" -> Input, and that node has no "value".
curl -s -X POST -H "Authorization: Basic $AUTH" -H "Content-Type: application/json" \
  -d '{"inputParams":{"apiKey":{"value":"anything","hidden":true}}}' \
  "http://localhost:8082/api/v1/apps/dotGoogleTranslate-config/$SITE"

Actual:

{"message":"Cannot invoke \"com.fasterxml.jackson.databind.JsonNode.asText()\" because \"value\" is null"}

with HTTP 500.

Expected: HTTP 400 naming the offending parameter, e.g. "Secret 'apiKey' is missing a value".

Sending {"apiKey":{"hidden":true}} (a genuinely value-less secret) reproduces it just as directly.

Why callers hit this easily

SecretForm's constructor is a single-argument @JsonCreator with no @JsonProperty, which makes it a delegating creator — so the request body must be the bare map:

{"apiKey": {"value": "...", "hidden": true}}

not the arguably more natural:

{"inputParams": {"apiKey": {"value": "...", "hidden": true}}}

Anyone who guesses the second shape — reasonable, since inputParams is the field name — gets an NPE-derived 500 rather than a message telling them the shape is wrong. It took three attempts to find the accepted form.

Impact
  • A malformed request produces a 500, so it reads as a server fault rather than a client error, and pollutes error dashboards.
  • The response leaks an internal JVM NPE message to the API consumer. Low severity — no secret material is exposed — but poor hygiene for an endpoint whose whole job is handling credentials.
  • No data is written or damaged: the failure happens during deserialization, before the secrets store is touched. Verified on a container — the store's checksum was unchanged after each failed attempt.
Acceptance Criteria
  • A body where an Input has no value returns 400, not 500, with a message naming the parameter.
  • The response contains no raw JVM exception text.
  • hidden keeps its current tolerant behaviour (absent → false).
  • A body wrapped in {"inputParams": {...}} returns a 400 explaining the expected shape rather than an NPE — or is accepted, if supporting both shapes is preferred.
  • Unit coverage for Input.InputDeserialize over: value present, value absent, hidden absent, and an empty object.
  • The accepted request shape is documented — the @Operation/@Schema annotations on AppsResource.createAppSecrets so it lands in the generated openapi.yaml.
Additional Context

Pre-existing on main; not introduced by any recent change. Found while manually exercising the Apps REST API to verify the secrets-store fix in #36724 / PR #37053, which is unrelated to this defect.

Severity

Low - Cosmetic or minor issue

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Input.InputDeserialize in dotCMS/src/main/java/com/dotcms/rest/api/v1/apps/Input.java and then inspect AppsResource.createAppSecrets, including its @Operation/@Schema annotations. Add unit coverage for present and missing value, absent hidden, and an empty object; done means malformed inputs return 400 without raw JVM text, while the accepted request shape is documented in openapi.yaml.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.