Skip to content

PUT page preserves user-supplied creator on existing-line updates #500

@thehabes

Description

@thehabes

Summary

The page PUT route lets a client override creator on an existing-line update. Authenticated callers can attribute a saved line to any agent IRI they choose. New lines are unaffected.

Where

page/index.js:119-123

const line = item.id?.startsWith?.('http')
    ? new Line(item)
    : Line.build(projectId, pageId, item, user.agent.split('/').pop())
line.creator ??= user.agent.split('/').pop()

For the http-id branch, new Line(item) reads creator from input via destructuring (classes/Line/Line.js:16-29). The ??= on the next line only fills when nullish, so an input-supplied creator is preserved. The Line.build branch is safe — its destructure (Line.js:32-36) drops creator from input and uses the function-arg from user.agent.

#saveLineToRerum then writes the line to RERUM via creator: await fetchUserAgent(this.creator). fetchUserAgent (utilities/shared.js:343-358) returns any string starting with http verbatim, so the supplied IRI flows through unchanged.

Reproduction

PUT a page with an existing-line update whose body carries a fake creator:

PUT /project/{projectId}/page/{pageId}
Authorization: Bearer <legitimate user token>
Content-Type: application/json

{
  "items": [
    {
      "id": "https://store.rerum.io/v1/id/<existing-line-id>",
      "body": [{ "type": "TextualBody", "value": "x", "format": "text/plain" }],
      "target": { /* unchanged */ },
      "creator": "https://store.rerum.io/v1/id/SOMEONE_ELSE"
    }
  ]
}

Returns 200. The new RERUM version of that line records creator: https://store.rerum.io/v1/id/SOMEONE_ELSE, not the authenticated user's agent.

Impact

  • Audit/provenance via __rerum.generatedBy is intact (RERUM stamps that from auth), so the underlying "who actually wrote this version" is recoverable.
  • The body-level creator is what consumers read for attribution display ("authored by X"). Anything that surfaces creator in the UI shows the wrong agent.
  • Authentication is required, so this isn't anonymous spoofing — but a token holder can attribute lines to other users, which has obvious abuse and audit-trail implications.
  • screenContentMiddleware / hasSuspiciousPageData don't normalize creator; the common_keys list checks for script injection in label/value/text/etc., not identity fields.

Suggested fix

Strip creator from input on the existing-line branch and force it from auth, mirroring how the page-level creator is set at page/index.js:95:

const line = item.id?.startsWith?.('http')
    ? new Line({ ...item, creator: undefined })
    : Line.build(projectId, pageId, item, user.agent.split('/').pop())
line.creator = user.agent.split('/').pop()

(Or hard-assign instead of ??= and document that creator is server-controlled, then strip in the constructor as a defense-in-depth.)

Worth a similar audit on the PUT /line/:lineId and PATCH routes — Object.assign(line, req.body) at line/index.js:130 has the same shape and likely the same exposure.

Found via

/static-review of TPEN-Prompts#4. The prompts UI accepts JSON pasted from an LLM and submits it via this PUT; an LLM that hallucinates a creator field would silently misattribute lines. The right defense is here, not in the client.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions