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.
Summary
The page PUT route lets a client override
creatoron 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-123For the
http-id branch,new Line(item)readscreatorfrom input via destructuring (classes/Line/Line.js:16-29). The??=on the next line only fills when nullish, so an input-suppliedcreatoris preserved. TheLine.buildbranch is safe — its destructure (Line.js:32-36) dropscreatorfrom input and uses the function-arg fromuser.agent.#saveLineToRerumthen writes the line to RERUM viacreator: await fetchUserAgent(this.creator).fetchUserAgent(utilities/shared.js:343-358) returns any string starting withhttpverbatim, so the supplied IRI flows through unchanged.Reproduction
PUT a page with an existing-line update whose body carries a fake
creator: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
__rerum.generatedByis intact (RERUM stamps that from auth), so the underlying "who actually wrote this version" is recoverable.creatoris what consumers read for attribution display ("authored by X"). Anything that surfaces creator in the UI shows the wrong agent.screenContentMiddleware/hasSuspiciousPageDatadon't normalizecreator; the common_keys list checks for script injection in label/value/text/etc., not identity fields.Suggested fix
Strip
creatorfrom input on the existing-line branch and force it from auth, mirroring how the page-level creator is set atpage/index.js:95:(Or hard-assign instead of
??=and document thatcreatoris server-controlled, then strip in the constructor as a defense-in-depth.)Worth a similar audit on the
PUT /line/:lineIdandPATCHroutes —Object.assign(line, req.body)atline/index.js:130has the same shape and likely the same exposure.Found via
/static-reviewof TPEN-Prompts#4. The prompts UI accepts JSON pasted from an LLM and submits it via this PUT; an LLM that hallucinates acreatorfield would silently misattribute lines. The right defense is here, not in the client.