Skip to content

MM-69978: validate card property types before persisting them - #243

Open
nang2049 wants to merge 1 commit into
mainfrom
MM-69978
Open

MM-69978: validate card property types before persisting them#243
nang2049 wants to merge 1 commit into
mainfrom
MM-69978

Conversation

@nang2049

@nang2049 nang2049 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

A board Editor could persist a non-string value into a card property name or a card property value which the
webapp then rendered directly as a React child. The result was a permanent crash for every user opening the affected board or card. This PR adds one shared validator in server/model/card_property.go that requires property values to be strings or arrays of strings, and property templates to have a non-empty string id plus string name, type and options fields.

  • BoardPatch.IsValid / Patch the originally reported endpoint
  • CardPatch.CheckValid / Patch and handlePatchCard the instance found during review now returning 400 instead of 500
  • ValidateBlockPatch PATCH /boards/{id}/blocks/{id}, the endpoint the webapp itself uses to edit property values, otherwise the card fix is trivially bypassable
  • Board.IsValid / Card.CheckValid the matching create endpoints

As defense in depth, and to make boards that are corrupted openable again, the webapp coerces property names and values before rendering them via safePropertyString / safePropertyValue in blocks/board.ts, applied centrally in propertyValueElement.tsx plus cardDetailProperties.tsx, tableHeaders.tsx andurl.tsx.

Manual QA Steps

Setup: User A (system admin) creates a board with a text property, adds a card, and shares the board with User B (regular user) as Editor. All requests below are sent as User B.

1. Board card property name is rejected

curl -i -X PATCH "$SERVER/plugins/focalboard/api/v2/boards/$BOARD_ID" \
  -H "Authorization: Bearer $USER_B_TOKEN" -H 'Content-Type: application/json' \
  -d '{"updatedCardProperties":[{"id":"'"$PROP_ID"'","name":{},"type":"text","options":[]}],
       "deletedCardProperties":[],"updatedProperties":{},"deletedProperties":[]}'

Expect 400 Bad Request (was 200). Confirm with a GET on the board that name is still
the original string, then open the board as User A: it loads normally in table and card view.

2. Card property value via the cards API is rejected

curl -i -X PATCH "$SERVER/plugins/focalboard/api/v2/cards/$CARD_ID" \
  -H "Authorization: Bearer $USER_B_TOKEN" -H 'Content-Type: application/json' \
  -d '{"updatedProperties":{"'"$PROP_ID"'":{}}}'

Expect 400 Bad Request. GET the card and confirm the previous value is intact.

3. Card property value via the blocks API is rejected

curl -i -X PATCH "$SERVER/plugins/focalboard/api/v2/boards/$BOARD_ID/blocks/$CARD_ID" \
  -H "Authorization: Bearer $USER_B_TOKEN" -H 'Content-Type: application/json' \
  -d '{"updatedFields":{"properties":{"'"$PROP_ID"'":{}}}}'

Expect 400 Bad Request.

Ticket Link

https://mattermost.atlassian.net/browse/MM-69978

Change Impact: 🟡 Medium

Regression Risk: Validation affects shared persistence and rendering paths across server and webapp layers; malformed data handling is covered, but valid edge cases could regress.

QA Recommendation: Manual QA can be skipped given comprehensive automated coverage; targeted smoke testing is optional.

Generated by CodeRabbitAI

@nang2049 nang2049 added 2: Dev Review Requires review by a core committer 3: QA Review Requires review by a QA tester labels Jul 30, 2026
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Card and board property payloads now receive server-side schema validation before patch application, with API and model tests covering rejected malformed values. Frontend helpers normalize invalid property names and values across board, card, table, and URL rendering.

Changes

Card property integrity

Layer / File(s) Summary
Card property validation contract
server/model/card_property.go, server/model/card_property_test.go
Adds validation for card property values, templates, options, and associated invalid-shape tests.
Model patch validation and application
server/model/card.go, server/model/board.go, server/model/block.go, server/model/card_property_test.go
Validates card, board, and block property updates and prevents invalid patch values from changing existing state.
API patch enforcement
server/api/cards.go, server/integrationtests/cards_test.go, server/integrationtests/board_test.go
Rejects invalid card patches with 400 Bad Request before applying them, with integration coverage for unchanged persistence.
Frontend property sanitization
webapp/src/blocks/board.ts, webapp/src/blocks/board.test.ts, webapp/src/components/..., webapp/src/properties/url/url.tsx
Adds shared sanitizers and applies them to property names, values, URL handling, editors, and table headers.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding validation for card property types before persistence.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch MM-69978

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/model/block.go (1)

203-207: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate property values on full block validation.

This path only verifies the outer map type. Unlike BlockPatch, it can still persist {"properties":{"prop":{}}} through full block creation/update validation. Call ValidateCardPropertyValues after the type assertion.

Proposed fix
 	if propsIface, present := b.Fields[BlockFieldProperties]; present {
-		if _, ok := propsIface.(map[string]interface{}); !ok {
+		props, ok := propsIface.(map[string]interface{})
+		if !ok {
 			return ErrBlockPropertiesInvalidType
 		}
+		if err := ValidateCardPropertyValues(props); err != nil {
+			return err
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/model/block.go` around lines 203 - 207, Update full block validation
in the block validation method containing the BlockFieldProperties type check to
call ValidateCardPropertyValues after the properties map type assertion
succeeds. Preserve ErrBlockPropertiesInvalidType for invalid outer values, and
reject invalid nested property values before full block creation or update
proceeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/api/cards.go`:
- Around line 300-303: In the handler flow before calling patch.CheckValid,
detect when the unmarshaled patch is nil, including a JSON null body, and return
a 400 Bad Request through a.errorResponse. Keep the existing validation and
error handling unchanged for non-nil patches.

In `@webapp/src/properties/url/url.tsx`:
- Around line 29-30: Initialize the URL editor’s value from the sanitized
propertyValue rather than the raw card property, ensuring Editable always
receives a string. Update the relevant URL editor state initialization near
safePropertyString, and add a regression test covering edit mode with a
malformed persisted URL object or array.

---

Outside diff comments:
In `@server/model/block.go`:
- Around line 203-207: Update full block validation in the block validation
method containing the BlockFieldProperties type check to call
ValidateCardPropertyValues after the properties map type assertion succeeds.
Preserve ErrBlockPropertiesInvalidType for invalid outer values, and reject
invalid nested property values before full block creation or update proceeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3c3949e1-5f7d-4140-9cf5-072e5c700a49

📥 Commits

Reviewing files that changed from the base of the PR and between 9c099ba and 8247e67.

📒 Files selected for processing (14)
  • server/api/cards.go
  • server/integrationtests/board_test.go
  • server/integrationtests/cards_test.go
  • server/model/block.go
  • server/model/board.go
  • server/model/card.go
  • server/model/card_property.go
  • server/model/card_property_test.go
  • webapp/src/blocks/board.test.ts
  • webapp/src/blocks/board.ts
  • webapp/src/components/cardDetail/cardDetailProperties.tsx
  • webapp/src/components/propertyValueElement.tsx
  • webapp/src/components/table/tableHeaders.tsx
  • webapp/src/properties/url/url.tsx

Comment thread server/api/cards.go
Comment on lines +300 to +303
if err = patch.CheckValid(); err != nil {
a.errorResponse(w, r, model.NewErrBadRequest(err.Error()))
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## cards.go excerpt\n'
nl -ba server/api/cards.go | sed -n '250,320p'

printf '\n## model patch type and methods\n'
rg -n "type CardPatch|func \\(.*CardPatch.*CheckValid|CheckValid\\(" server model -g '*.go'

printf '\n## surrounding model file excerpt\n'
file=$(rg -l "type CardPatch" server model -g '*.go' | head -n 1)
if [ -n "${file:-}" ]; then
  nl -ba "$file" | sed -n '1,220p'
fi

Repository: mattermost/mattermost-plugin-boards

Length of output: 234


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## cards.go excerpt\n'
python3 - <<'PY'
from pathlib import Path
p = Path('server/api/cards.go')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 250 <= i <= 320:
        print(f"{i:4d}: {line}")
PY

printf '\n## model patch type and methods\n'
rg -n "type CardPatch|func \\(.*CardPatch.*CheckValid|CheckValid\\(" server model -g '*.go'

printf '\n## candidate file contents\n'
file=$(rg -l "type CardPatch" server model -g '*.go' | head -n 1)
if [ -n "${file:-}" ]; then
  python3 - <<'PY'
from pathlib import Path
import os
file = os.environ['file']
for i, line in enumerate(Path(file).read_text().splitlines(), 1):
    if i <= 220:
        print(f"{i:4d}: {line}")
PY
fi

Repository: mattermost/mattermost-plugin-boards

Length of output: 3351


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## cards.go excerpt\n'
python3 - <<'PY'
from pathlib import Path
p = Path('server/api/cards.go')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 250 <= i <= 320:
        print(f"{i:4d}: {line}")
PY

printf '\n## model patch type and methods\n'
rg -n "type CardPatch|func \\(.*CardPatch.*CheckValid|CheckValid\\(" server model -g '*.go'

printf '\n## candidate file contents\n'
file=$(rg -l "type CardPatch" server model -g '*.go' | head -n 1)
if [ -n "${file:-}" ]; then
  FILE="$file" python3 - <<'PY'
from pathlib import Path
import os
file = os.environ['FILE']
for i, line in enumerate(Path(file).read_text().splitlines(), 1):
    if i <= 220:
        print(f"{i:4d}: {line}")
PY
fi

Repository: mattermost/mattermost-plugin-boards

Length of output: 3351


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('server/model/card.go')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 145 <= i <= 230 or 280 <= i <= 320:
        print(f"{i:4d}: {line}")
PY

Repository: mattermost/mattermost-plugin-boards

Length of output: 4260


Reject null before validating the patch.

json.Unmarshal leaves patch nil for a JSON null, so patch.CheckValid() panics here. Return 400 Bad Request when the body is null.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/cards.go` around lines 300 - 303, In the handler flow before
calling patch.CheckValid, detect when the unmarshaled patch is nil, including a
JSON null body, and return a 400 Bad Request through a.errorResponse. Keep the
existing validation and error handling unchanged for non-nil patches.

Comment on lines +29 to +30
const propertyValue = safePropertyString(props.propertyValue)
const isEmpty = !propertyValue.trim()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Initialize the URL editor state from the sanitized value.

propertyValue is safe, but value is still initialized from the raw card property immediately above. A corrupted object or array can therefore reach Editable as a non-string and bypass the new safety boundary.

Proposed fix
-    const [value, setValue] = useState(props.card.fields.properties[props.propertyTemplate.id || ''] || '')
+    const [value, setValue] = useState(safePropertyString(props.propertyValue))

Add a regression test for entering edit mode with a malformed persisted URL value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/properties/url/url.tsx` around lines 29 - 30, Initialize the URL
editor’s value from the sanitized propertyValue rather than the raw card
property, ensuring Editable always receives a string. Update the relevant URL
editor state initialization near safePropertyString, and add a regression test
covering edit mode with a malformed persisted URL object or array.

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

Labels

2: Dev Review Requires review by a core committer 3: QA Review Requires review by a QA tester

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant