Skip to content

Refuse to export a demo instance - #846

Merged
blaipr merged 1 commit into
mainfrom
fix/a-demo-does-not-export-itself
Aug 23, 2026
Merged

blaipr merged 1 commit into
mainfrom
fix/a-demo-does-not-export-itself

Conversation

@blaipr

@blaipr blaipr commented Aug 23, 2026

Copy link
Copy Markdown
Member

The defect

Every way of getting an installation out of a demo instance refuses — except the web export, both
halves of it.

demo guard
Web backup, and both backup downloads ✓
Web config import, encryption save + refresh ✓
REST config/export ✓ (#831)
Web xmlExport (create) ✗
Web downloadExport (fetch) ✗

A demo publishes its administrator's credentials by design, so the ACL in front of these stops
nobody and the demo check is the whole boundary. What the export contains is the same installation
the backup is guarded for: every account's encrypted secret and its key, and — when no export
password is given — the name, login, URL and notes of every account in the clear
(XmlExport::appendNode() only encrypts when a password is supplied; established in #840).

#831 fixed the REST door by comparing it against the five web config actions that had the guard.
The web export was not among those five, so comparing against them could not reveal it — the gap was
in the list I was comparing to.

Both halves are guarded, because guarding only the creation leaves an export made before the
instance became a demo fetchable, which is a way around it.

Test

ConfigBackupTest gains both refusals. Mutation-checked: reverting src/ fails exactly those two.

The download refusal asserts an empty body rather than a message, which is what the backup
downloads beside it already answer on a demo: these actions are typed ResponseType::CALLBACK, so a
refusal that is not a callable renders as nothing. That blank page is a pre-existing wart the three
share — I verified it against the untouched downloadBackupApp rather than assuming — and it is
left alone here. What the test pins is the part that matters: the export is not handed over.

OK (4043 tests, 36987 assertions)   unit
OK (1008 tests, 3047 assertions)    integration

PHPStan level 6 and PHPCS clean.

Correcting the record on #841

CLAUDE.md is corrected, and the correction is the substantive part of this PR's docs.

#841 said any signed-in user could delete another user's notification, because Acl returns true
for the notification actions unconditionally. NOTIFICATION_VIEW, NOTIFICATION_SEARCH and
NOTIFICATION_CHECK are in that list — NOTIFICATION_DELETE is not in the switch at all, so it
falls through to the deny at the end, and only isAdminApp ever reached those methods.

The ownership check that PR added is still right — as defence in depth, and for consistency with
setCheckedById() — but it was not the reachable hole it was described as. The lesson recorded
alongside it: read the ACL arm for the exact action, not for its neighbours.

Every way of getting an installation out of a demo refuses — the backup and both
of its downloads, the config import, the encryption save and refresh, and the
REST export since #831 — except the web export, both halves of it.

A demo publishes its administrator's credentials by design, so the ACL in front
of these stops nobody and the demo check is the whole boundary. The export holds
the same installation the backup is guarded for: every account's encrypted secret
and its key, and, when no export password is given, the name, login, URL and
notes of every account in the clear.

#831 fixed the REST door by comparing it against the five web config actions that
had the guard. The web export was not among those five, so comparing against them
could not reveal it: the gap was in the list being compared to.

Both halves are guarded, because guarding only the creation leaves an export made
before the instance became a demo fetchable, which is a way around it.

The download refusal asserts an empty body rather than a message, which is what
the backup downloads beside it already answer on a demo: these actions are typed
CALLBACK, so a refusal that is not a callable renders as nothing. That blank page
is a pre-existing wart the three share, verified against the untouched
downloadBackupApp rather than assumed, and left alone. What the test pins is that
the export is not handed over.

CLAUDE.md corrects #841, which said any signed-in user could delete another
user's notification on the grounds that Acl returns true for the notification
actions unconditionally. NOTIFICATION_VIEW, NOTIFICATION_SEARCH and
NOTIFICATION_CHECK are in that list; NOTIFICATION_DELETE is not in the switch at
all and falls through to the deny, so only isAdminApp ever reached those methods.
The ownership check remains right as defence in depth, but it was not the
reachable hole it was described as.
@blaipr
blaipr merged commit 81c3f9c into main Aug 23, 2026
8 checks passed
@blaipr
blaipr deleted the fix/a-demo-does-not-export-itself branch August 23, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant