A read-only accountant could cancel invoices and erase payment history in the open-source bookkeeping tool Akaunting, exposing small businesses to silent data loss. The flaw was patched in version 3.2.0, but the mistake—tying permission checks to a hard-coded list of method names—still threatens any system that relies on role-based access control.

How the bug slipped through

Akaunting’s API validates a user’s rights by consulting an allowlist of method names. The list covered the usual CRUD operations—create, read, update, delete—but missed several status-changing endpoints:

  • markSent
  • markCancelled
  • markReceived

Because those handlers weren’t on the list, the framework never called the permission-checking routine when they ran. A user with a read-only role could issue a simple GET request to the markCancelled endpoint and the system would treat it as a legitimate state change.

Cancelling an invoice does more than flag the document as void; it also removes any payment records linked to that invoice. The result: a user without edit rights can wipe out the financial trail of a transaction.

What the testing showed

The vulnerability appeared in the official Docker image of Akaunting:

  • A standard PUT request to update an invoice returned 403 Forbidden, confirming that the regular update path was protected.
  • A GET request to the cancellation endpoint succeeded with no authorization error, exposing the gap.

Why it matters

Financial statements can be altered without a clear audit trail, making fraud harder to spot and honest mistakes harder to fix.

The fix

Version 3.2.0 expands the permission map to include the previously omitted status actions. From that release onward, any request that changes a document’s state—whether marked sent, cancelled, or received—must pass through the same role verification as a standard update. This restores the expectation that a read-only role truly cannot modify data.

Lessons for developers

  • Never equate method names with safety. Adding a new endpoint does not automatically inherit protection; audit each public method for side effects.
  • Allowlists are only as complete as the list itself. A static list of “good” verbs leaves an open door for oversights.
  • Separate intent from HTTP verb. GET is meant to be read-only, but here it performed a state change. Restrict mutations to POST, PUT, DELETE, PATCH.
  • Automate permission coverage checks. Static analysis tools can flag controller methods lacking an authorization call, catching gaps before they ship.
  • Test with least-privilege accounts. The Docker-based test used a read-only user; replicating such scenarios in CI pipelines surfaces similar issues early.

What to watch next

Akaunting’s community has already released the patched version. Administrators should verify their instance’s version and apply the update promptly.

For developers building any role-based system, the takeaway is clear: a permission model that depends on remembering every possible action is fragile by design. Explicitly declare which operations modify state, enforce checks at the framework level, and regularly audit the codebase. Only then can a “read-only” label be trusted to keep financial records intact.