Three years ago, in the summer of 2023, Uppvidinge kommun had just started using Quiculum, the new school platform intended for use across the entire municipality.
These types of platforms generally pique my interest. The providers behind them process data belonging to a lot of people, including children. From a data protection, privacy and security standpoint, that is interesting to me.
I did not, however, open Quiculum for the first time with the intention of finding massive data breaches. I opened it to familiarise myself with the tool I was being forced to use for the foreseeable future.
School platforms generally handle many functions tied to a school: schedules, grades, curricula, notes, information, appointment booking, lunch menus, blog-like features and so on.
This particular system claimed to be a "one ring to rule them all" type of application. It was supposed to handle not only the general features, but everything from preschool through year 9 and adult education, as well as the principal and teacher backends and everything in between.
My first impression
I was given a guardian account. My children attended preschool at the time, so none of them had their own accounts.
With that account, I began exploring the application. My first impression was that the UI looked dated as hell and the UX was a freaking mess. I have used my fair share of badly designed applications, and this is definitely one I will remember in a less-than-positive way.
I tried to figure out how to put my children's schedule into the system so the preschool knew when they were supposed to be present.
Because of my wife's schedule and mine, our children followed a five-week schedule. Every five weeks, the schedule was supposed to repeat.
There was no straightforward way to do this.
In an ideal world, I would have been able to configure five individual weeks and set the entire five-week period to repeat. Another option would have been to repeat each day every five weeks.
Instead, the UX was, and truthfully probably still is, a complete mess. Entering a schedule was insanely cumbersome.
Other parts of the UI and UX were equally frustrating. Notifications could not be clicked to open the thing they were notifying you about. Things were named inconsistently, making it difficult to find what you were looking for. Those are just two examples.
How did Uppvidinge choose this?
I began asking questions of the municipality, Uppvidinge kommun.
Who ordered this? Why? Who tested it? What were the criteria?
To my absolute surprise, the municipality did not have anything to show for it. It stated that the process of evaluating, testing and reviewing different applications had taken years, yet not a single note or proper document from that process existed.
That, of course, made me quite mad.
The municipality is supposed to act in the best interests of its taxpayers and residents. In my eyes, this looked bad.
This was the beginning of my problems with Uppvidinge kommun. Since then, there have been several public-record requests, emails back and forth, meetings and other discussions about the matter. That is beyond the scope of this blog post.
The fact remained that Quiculum was there to stay. The contract was in place, and I had to use it.
So use it I did.
I only wanted a better way to enter the schedule
I downloaded the Android APK and ran it in a custom virtual machine that I use to dissect applications and capture traffic to and from them.
I fired up Burp Suite and began looking at the requests sent to and from both the application and the website.
Burp Suite can act as a proxy between an application and its server. Instead of only seeing what the interface chooses to display, I can inspect the HTTP requests the application sends and the responses the server returns.
This does not magically create access. It shows the communication the application is already making as my logged-in user.
My goal was to find the correct API calls so I could write a script to insert, change and update my children's schedules more quickly and efficiently.
That was the beginning of Quiculum's downfall in Uppvidinge kommun.
Not because that was what I had set out to do, but because of what those requests contained.
Authentication is not authorisation
To make what follows easier to understand, I need to give a brief and very zoomed-out overview of how these types of applications generally work.
First, we have a login, which is the authentication layer:
Who am I?
Then we have the role and permission checks, which are the authorisation layer:
Now that you know who I am, what am I allowed to see and do?
The browser and application cannot be trusted to enforce that second part. Anything sent to a user's device can be inspected or changed by that user. The server must therefore check the customer, account role, relationship and requested object every time it receives a request.
If a guardian asks for a child record, the server should verify that the child belongs to that guardian before returning anything. If the relationship is missing, the answer should be access denied.
When an application uses predictable object identifiers but fails to check whether the logged-in user is allowed to access the requested object, the problem is commonly called IDOR or broken object-level authorisation, BOLA.
The exact label is not important. The important part is that knowing an object's URL or identifier must never be enough to access it.
Every action I took within Quiculum was performed as myself: Martin Gravestam, guardian.
I never pretended to be someone else. I never borrowed or stole somebody's identity.
Logged in as myself, a guardian of children in preschool, one would assume that the system would authorise me to view information about my children, their schedules, their guardians, myself included, and other information relevant and limited to them.
Maybe that would include their class list, teachers' names, work contact information and things of that nature.
That assumption would be correct.
That, however, was not all I was greeted with.
The first data leak
Instead, I was greeted with large JSON responses containing data.
That data included a selection of schools, classes, children's names, teachers and guardians. It included schools and classes with which neither my children nor I had any association.
The data came only from within Uppvidinge kommun. I did not access anything belonging to another Quiculum customer.
This first set of data did not contain addresses or personal identity numbers. I would classify the leak as bad, but not "shit just hit the fan" bad.
It was definitely a breach that warranted action by the data controller, Uppvidinge kommun, in my opinion.
I found this breach on 13 July 2023. That same day, I emailed the municipality and attached my findings.
The following day, UppCom, the municipally owned broadband provider and IT support organisation, called me. I explained what I had found and how I had found it.
UppCom then spoke to Quiculum. Quiculum later called me, and I again explained what I had found and how I had found it. Later that day, I emailed Quiculum instructions for reproducing the problem.
This data was fed to me as plain JSON in an ordinary, inspectable request made when my logged-in guardian account visited the webpage.
They had relied on the frontend to display only the relevant parts, while the client application had all the data sitting openly in the background.
JSON is simply a structured text format. The fact that the ordinary webpage did not visibly print every field did not make the response private. Once the server sent the JSON to my browser, my account had received the data.
There were therefore two problems. The server returned more data than the page needed, and it returned objects outside the scope of my children. Filtering the response visually in the frontend did not fix either problem.
That is insane. Absolutely insane.
Nevertheless, Quiculum wrote back later that day and said it had fixed the issue and that names were no longer part of the output.
I revisited the same API calls and confirmed that the names had been removed.
Even though I thought the whole thing was absolutely mental, it could have been an honest mistake.
I was probably naive to think that Quiculum had gone through all of its API calls and fixed any other issues that might have been there.
Taking Quiculum's answer literally
Fast-forward to April 2024, almost a year after the previous incident.
I had been thinking about that incident for a while. The way the data had been provided to the client, the request that caused it and Quiculum's statement that it had "removed the names from that call" left me with a nagging feeling.
Maybe I should take Quiculum's statement literally.
Maybe it had removed the names from that one call, and that was it.
Maybe it had not reviewed the rest of the system, all possible calls or its authorisation model.
My feeling was that this could be a systemic failure rather than an isolated error.
I had some spare time and decided to go through the application again. I looked at the code the client application received from Quiculum's servers, the API endpoints it provided to me and the URLs it openly exposed to me.
The important part is that I looked at what Quiculum provided to my client application. I did not brute-force my way into anything.
The client code exposed endpoint and URL patterns. Some objects used numeric identifiers, making it possible to request another object by changing the identifier. A properly designed server should still reject that request. Predictability is only dangerous when the authorisation check is missing.
It did not take much effort to find more endpoints that simply provided me with data.
I also found direct URLs to pages intended for school administrators.
The existence of those endpoints and URLs did not raise any eyebrows by itself. The fact that some of them worked and allowed me to access more data and functions certainly did.
This time, I was again looking at more students, guardians, schools and related information. In addition, there were administrative functions concerning teacher planning, school units and things of that nature.
I do not know whether I could actually have changed anything through those pages.
I decided not to risk damaging their data in case the functions worked. Doing so would probably also have entered a very grey legal area.
My purpose was not to destroy anything. It was to find out whether my hunch was correct.
Reporting it again
I reported this leak directly to the data controller, Uppvidinge kommun. Quiculum later "fixed" it.
By this point, I had absolutely no trust in Quiculum. My patience with the municipality was also reaching its limit for several reasons, including its handling of these incidents.
I reached out to all the customers Quiculum listed on its website, which included schools in different parts of Sweden.
Some of them responded. I had a conversation about the issue with one of them.
In my eyes, Quiculum and the municipality had made the leaks sound like isolated events: one user had gained access to this, and so on.
What I wanted to demonstrate was that the root cause had nothing to do with Uppvidinge kommun. It was a core problem in the Quiculum platform.
I had never gained access to data belonging outside Uppvidinge. This led me to believe that there was a hard separation between customer datasets, thank God. It was probably implemented through separate databases for each customer or something to that effect.
The person I spoke to at another school was an administrator.
He tested a guardian account using URLs I provided. He already had access to those pages through his ordinary administrator account, but the guardian account could view the same material.
This confirmed my thesis that it was a system design flaw rather than an isolated incident.
My Uppvidinge account did not cross into his school's data, and his guardian account did not cross into Uppvidinge. The customer data appeared to be separated. The same broken role boundary, however, existed inside two separate customer environments.
As stated previously, Quiculum fixed that particular issue.
The "oh my fucking god" moment
I kept examining other endpoints Quiculum provided to my client application.
Eventually, I stumbled upon an "oh my fucking god" moment.
Logged in as myself, a guardian of children at a preschool, I was given JSON files containing students, both active and inactive, addresses, guardians, schools, classes, personal identity numbers, temporary identity numbers used for people without ordinary Swedish personal identity numbers, including immigrants, and notes written by teachers about students.
The raw material contained 3,674 child records and 3,508 unique populated child identifiers. Of those identifiers, 3,443 looked like 12-digit Swedish personal identity numbers, 55 were temporary TF identifiers and 10 used other formats.
The distinction between records and people matters. The data contained duplicates, historical entries and inactive students. Of the unique identifiers, 1,649 were marked active.
There were address lines linked to 3,212 unique identifiers, guardian records linked to 1,682 and note or information fields linked to 135.
Based on the number of unique individuals and the range of schools in those files, I would make an educated guess that we were looking at every student in Uppvidinge, regardless of school.
The data ranged from preschool and school years 1 through 9 to adult education.
Statistics Sweden recorded 8,897 residents in Uppvidinge during the first quarter of 2026, so I would say the numbers add up fairly well.
That is fucking mental.
How the fuck did they not fix this in 2023, when I initially started finding and reporting these problems?
Why this matters beyond a technical mistake
Most guardians do not care about JSON, APIs, Burp Suite, IDOR or authorisation models.
They should not have to.
They want to enter a schedule, read information from school and move on with their day. They should be able to assume that their account only has access to their own family.
A personal identity number cannot simply be changed like a password. An address connected to a child, school, class and guardian relationships says far more than any of those data points in isolation.
Notes written by teachers were intended for a limited school context. They were not written for an unrelated guardian who happened to inspect a server response.
Children also had no choice. They did not select Quiculum, approve the contract or decide which information the municipality stored there.
Privacy is not only about hiding something embarrassing. It is about controlling who can collect, combine and use information about you. That is especially important when the people involved are children.
Blocking requests instead of fixing the cause
Instead of fixing the core issues, Quiculum installed what I consider false protection in the form of banning accounts that made too many "bad requests".
Because testing the endpoints Quiculum provided to my client application inevitably produced some "bad requests", my account was banned for a while.
Rate limiting and automatic account locks can be useful additional protections. They are not authorisation.
The first unauthorised request should return an access-denied response, usually HTTP 403, without exposing any data. Locking the account after several requests does not repair a server that answered the requests it should have denied.
This company has been around for more than a decade.
It is insane to think that, after about a decade "in the game", it had never addressed these issues internally.
I will not speculate about how much data might have been leaked in the past from different customers, but you are welcome to do so.
What happened afterwards
Uppvidinge eventually got rid of Quiculum.
Since 2025, Uppvidinge has used different school platforms.
Let us hope we do not have to revisit this issue again.
I reported every incident described above through the appropriate channels. I made reports to IMY after each incident, and Uppvidinge kommun filed its own reports as well.
I never publicly exposed any of the data.
The data gathered during my testing was shredded a long time ago. It was overwritten multiple times and deleted, not merely deleted.
Sveriges Radio and Smålandsposten provided some limited coverage of the incidents, although neither spoke to me.
The records are public
The IMY documents are public records. Anyone can contact IMY and request them.
Uppvidinge kommun's documents are also public records. Anyone can contact the municipality and request them.
If anyone reading this is considering Quiculum as a provider of school software, just do not.
Its product is absolute garbage.
All I wanted was a more efficient schedule
It is interesting how the mere pursuit of making something more efficient for myself could lead to all this nonsense.
It is also very telling.
If we do not examine the products we buy and the services we use with a critical eye, privacy can easily go down the drain.