Skip to content

Wallpaper renderer trusts local storage before server-confirmed activation #420

Description

@Benyaminrmb

Summary

The wallpaper renderer trusts locally stored StoredWallpaper data and wallpaper_change event payloads without verifying that a server-backed wallpaper was successfully activated.

A locally supplied synthetic wallpaper can therefore be rendered without a successful response from:

PUT /wallpapers/@me

This report concerns the client-side trust boundary only. I have not confirmed a server-side ownership or coin bypass, and I did not download or redistribute paid assets.

Affected files

  • src/layouts/setting/tabs/wallpapers/hooks/use-wallpaper.tsx
  • src/layouts/setting/tabs/wallpapers/hooks/use-wallpaper-apply.tsx
  • src/context/wallpaper.context.tsx
  • background/wallpaper-cache.ts

Observed behavior

useWallpaperApply() reads the local wallpaper value and passes it directly to applyWallpaper().

The same renderer accepts wallpaper_change event payloads containing:

{
  id: string
  type: "IMAGE" | "VIDEO" | "GRADIENT"
  src: string
}

The rendering path does not distinguish between:

  • A custom local wallpaper
  • A free server wallpaper
  • An owned paid wallpaper
  • An unconfirmed server wallpaper
  • Arbitrary locally supplied wallpaper state

It does not check authentication, ownership, entitlement, or whether PUT /wallpapers/@me succeeded.

Additionally, when wallpaper.isOwned === true, handleSelectBackground() updates the local wallpaper before the activation request completes. If the request fails, the previous wallpaper is not restored.

Safe reproduction

I reproduced the behavior using a generated synthetic image/video with a fake ID:

  1. Back up the existing local wallpaper value.
  2. Store a synthetic StoredWallpaper, or dispatch it using wallpaper_change.
  3. Observe that the renderer immediately applies it.
  4. Confirm that no PUT /wallpapers/@me request occurred.
  5. Reload offline and observe that the locally stored wallpaper is applied.
  6. Restore the original wallpaper.

No paid wallpaper URL or protected asset is required for this reproduction.

I can provide the screen-capture PoC privately to a maintainer.

Expected behavior

Server-backed paid wallpapers should only become persistent active state after successful server confirmation.

If an activation request fails, optimistic local state should be reverted to the previously authorized wallpaper.

Local custom wallpapers should also be explicitly distinguished from server-backed wallpapers.

Actual behavior

Locally supplied StoredWallpaper data is applied directly. The renderer does not know whether the wallpaper was:

  • Authorized by the server
  • Purchased
  • Owned
  • A temporary preview
  • Locally created

Security classification

This is currently a confirmed client-side/local-state trust issue—not a confirmed server-side payment bypass.

Because browser-extension storage is controlled by the local user, the direct security severity may be low unless full-quality paid asset URLs are exposed through another weakness.

A server-side authorization vulnerability would require separate evidence that an unowned account can make PUT /wallpapers/@me persist a paid wallpaper without a legitimate purchase. I have not demonstrated or claimed that behavior.

Suggested remediation

  • Apply server-backed wallpapers only after PUT /wallpapers/@me succeeds.
  • Restore the previous wallpaper when activation fails.
  • Separate local custom wallpaper state from server-entitled wallpaper state.
  • Never treat client-side coin or isOwned values as authorization controls.
  • Consider storing only the wallpaper ID for server-backed selections.
  • Resolve entitled wallpaper data through account synchronization.
  • Mark preview state explicitly and never persist it as an owned wallpaper.
  • Validate or restrict remote URLs accepted by the offline wallpaper cache.

Environment

  • Repository: widgetify-app/widgetify-extension
  • Audited commit: 8958250eec27bebf07c02e13448211879f69b444
  • Testing performed only with my own account and synthetic local assets.

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