Measured while implementing the ruled fix for #15964 (option A, 2026-09-06). The triage comment on that card asked this question explicitly and recorded it as untested:
⚠️ 无论哪一支,都请一并回答一个本席在源码上看到、卡片没展开的问题::1117 的 updated_by 也带 preserveAudit 分支,而 created_by(若存在)在本席读到的这段里没有出现。⇒ created_by 是否也有同样的洞?
It has. Measured, not inferred.
The reading
Rig: a real ObjectKernel + ObjectQLPlugin (so the shipped sys_stamp_audit_insert hook is bound through bindHooksToEngine) over a capturing driver, so what reaches driver.create is the stored row. Object declares id (readonly) and title; created_at / created_by / updated_by are the registry-injected AUDIT_FIELD_DEFS shapes, all readonly: true.
row0: title=a created_by=forged_user updated_by=real_user session { userId: 'real_user' }
row1: title=b created_by=undefined updated_by=undefined session {} (no userId)
row2: title=c created_by=forged_user updated_by=real_user session { userId: 'real_user' }
- row0 / row2: an ordinary authenticated caller sent
created_by: 'forged_user' and it landed, while the session's real user was real_user. updated_by in the same payload was overwritten with real_user — the in-experiment control that proves the strip ran on this row and took the sibling audit field.
- row1 is the second control: with no
session.userId the hook's if (session?.userId) guard is false, nothing is assigned, and the engine strip then takes the forged value correctly (undefined). So the hole is created by the hook's assignment, exactly as on created_at.
Mechanism — identical to #15964, one field over
packages/objectql/src/plugin.ts, the audit binder's beforeInsert:
if (isInsert && hasField(objectName, 'created_by')) {
record.created_by = record.created_by ?? session.userId;
}
if (hasField(objectName, 'updated_by')) {
record.updated_by = preserveAudit ? (record.updated_by ?? session.userId) : session.userId;
}
created_by is ?? (client-preferred, ungated); updated_by is the preserveAudit ternary. Since #15395 the static-readonly strip runs inside engine.insert AFTER the hooks, and #14259's guard treats a key a beforeInsert hook ASSIGNED as the hook's write rather than a caller forgery (rowHookWrittenKeys) — so the ?? launders the caller's value past it. That is the same two-part mechanism #15964 documented for created_at.
Why it is filed rather than fixed in that PR
The 2026-09-06 ruling (decision batch #54) adopted option A for created_at and named only that field. The triage seat was explicit that it does not claim created_by is broken, only that it had not been measured. It now has been. Fixing it is the same one-line shape change (record.created_by = preserveAudit ? (record.created_by ?? session.userId) : session.userId;) but it is a decision about a different field's write contract, so it is a card, not a rider on a ruled PR.
⚠️ Note that created_by is NOT symmetric with created_at in one respect worth deciding on: the created_at stamp is unconditional (driver-sql provisions the column on every table), while created_by is guarded by hasField and by session.userId. A shape decision has to say what an insert with no session should do — today it leaves the caller's forgery for the strip to take, which is the correct outcome and happens for a different reason.
Severity
Same class as #15964 and reached the same way — a plain authenticated POST with no flag and no privilege — but about attribution rather than timing: a row can claim it was created by a user who did not create it. Audit integrity, not privilege escalation. Grading is triage's; this card states the measurement.
Filed from the #15964 round. Fix lands in packages/objectql/src/plugin.ts, the same function.
Generated by Claude Code
Measured while implementing the ruled fix for #15964 (option A, 2026-09-06). The triage comment on that card asked this question explicitly and recorded it as untested:
It has. Measured, not inferred.
The reading
Rig: a real
ObjectKernel+ObjectQLPlugin(so the shippedsys_stamp_audit_inserthook is bound throughbindHooksToEngine) over a capturing driver, so what reachesdriver.createis the stored row. Object declaresid(readonly) andtitle;created_at/created_by/updated_byare the registry-injectedAUDIT_FIELD_DEFSshapes, allreadonly: true.created_by: 'forged_user'and it landed, while the session's real user wasreal_user.updated_byin the same payload was overwritten withreal_user— the in-experiment control that proves the strip ran on this row and took the sibling audit field.session.userIdthe hook'sif (session?.userId)guard is false, nothing is assigned, and the engine strip then takes the forged value correctly (undefined). So the hole is created by the hook's assignment, exactly as oncreated_at.Mechanism — identical to #15964, one field over
packages/objectql/src/plugin.ts, the audit binder'sbeforeInsert:created_byis??(client-preferred, ungated);updated_byis thepreserveAuditternary. Since #15395 the static-readonlystrip runs insideengine.insertAFTER the hooks, and #14259's guard treats a key abeforeInserthook ASSIGNED as the hook's write rather than a caller forgery (rowHookWrittenKeys) — so the??launders the caller's value past it. That is the same two-part mechanism #15964 documented forcreated_at.Why it is filed rather than fixed in that PR
The 2026-09-06 ruling (decision batch #54) adopted option A for
created_atand named only that field. The triage seat was explicit that it does not claimcreated_byis broken, only that it had not been measured. It now has been. Fixing it is the same one-line shape change (record.created_by = preserveAudit ? (record.created_by ?? session.userId) : session.userId;) but it is a decision about a different field's write contract, so it is a card, not a rider on a ruled PR.created_byis NOT symmetric withcreated_atin one respect worth deciding on: thecreated_atstamp is unconditional (driver-sql provisions the column on every table), whilecreated_byis guarded byhasFieldand bysession.userId. A shape decision has to say what an insert with no session should do — today it leaves the caller's forgery for the strip to take, which is the correct outcome and happens for a different reason.Severity
Same class as #15964 and reached the same way — a plain authenticated
POSTwith no flag and no privilege — but about attribution rather than timing: a row can claim it was created by a user who did not create it. Audit integrity, not privilege escalation. Grading is triage's; this card states the measurement.Filed from the #15964 round. Fix lands in
packages/objectql/src/plugin.ts, the same function.Generated by Claude Code