Doctor in the cloud: how we created a telemedicine service to fight coronavirus in Luxembourg

We started developing the platform … by accident. It all started with a pandemic, due to which the Luxembourg government ordered the creation of an online consultation service with doctors. In order to squeeze the maximum benefit from the platform, they decided to add everything that is needed for the full-fledged work of doctors with patients. The exchange of all kinds of medical documents and even the issuance of prescriptions for medicines were no exception – the old vendor immediately put everything into the service. And then, in order for all the goodness to be safe, they turned to us with a ready-made product – all we needed was to deploy the platform in a secure point of presence of the cloud.

A month and a half later, the service successfully worked in the Luxembourg EDH Tier IV data center, but even at the first meeting, we recognized it as a weak link in the entire project: unlike the point of presence, the platform could not boast of security and had a dozen other shortcomings. The decision was obvious – the service had to be done from scratch. Remained to convince the government of Luxembourg.

This doctor is broken, bring a new one: why we decided to make a new platform

To suggest putting a bullet on the old platform, we just had to glance at it. Instead of fixing all possible security issues and updating the design, the service was easier to build from scratch, and we had three good reasons for that.

1. The system was developed without a framework

Because of this, there were an unthinkable number of problems in the platform. If some popular framework were used to create a service – Symfony, Laravel, Yii or something else – then even mediocre developers would avoid most of the security problems, because ORMs can prepare queries to the database, templating engines can endcode the content received from the user, and forms are protected by default with CSRF tokens, and authorization and authentication are usually available almost out of the box. In the same case, the platform made our developer nostalgic for the student days – the code looked almost the same as his first lab work at the university.

For example, here’s how the database connection was implemented. The connection credentials were hardcoded above in the same file.

if (!isset($db)) {
	$db = new mysqli($db_info['host'], $db_info['user'], $db_info['pass'], $db_info['db']);
	if ($db->connect_errno) {
		die("Failed to connect to MySQL: " . $db->connect_errno);
	if (!$db->set_charset("utf8")) {
		die("Error loading character set utf8 for MySQL: " . $db->connect_errno);

2. There were many security issues in the platform

After the audit, we realized that with such flaws it is impossible to go into production even with a simple blog, let alone a platform with confidential data. Here are some of them.

  • SQL injection. 90% of requests included data entered by users without preliminary preparation.
    $sql = "
    	UPDATE user
    	SET firstname="%s", lastname="%s", born='%s', prefix='%s', phone="%s", country_res="%s", extra=%s
    	WHERE id=%d
    $result = $db->query(sprintf($sql,
    	isset($_POST['extra']) ? "'".$_POST['extra']."'" : "NULL",
  • XSS vulnerabilities. The custom code was not filtered in any way before the output:
    <button id="btn-doc-password" class="btn btn-primary btn-large pull-right" data-action="<?= $_GET['action'] ?>"><i class="fas fa-check"></i> <?= _e("Valider") ?></button>

    In addition, the information that got into the database, such as the reason for consulting a doctor, was not filtered either before writing to the database or before rendering on the page.

  • Lack of verification of access rights. It was enough to have a patient account and pick up the auto-incremental ID of another patient to easily get a list of appointments and other information from the profile. The same problems were on the side of the doctor’s application. Moreover, knowing the doctor’s ID was not a problem to get access to his appointments.
  • Lack of verification of downloaded files. Through the form for adding documents, it was possible to upload any file to any of the folders available for writing to users of the web server. In addition, by playing with the quеry-string of the document upload request, one could even download the source code files.
    $file_dir = $settings['documents']['dir'] . $_SESSION['client']['id'] . DIRECTORY_SEPARATOR . $_GET['id_user'];
  • Outdated third party libraries. At the old vendor, no one followed the versions of third-party libraries, which, by the way, instead of using the same Composer, were simply copied into the project. Moreover, some of these third-party dependencies have been customized.
  • Insecure storage of user passwords. Unreliable cryptographic functions were used to store passwords.
    $sql = "
    	SELECT id, firstname, lastname
    	FROM user
    	WHERE id=%d AND password=PASSWORD('%s')
    $result = $db->query(sprintf($sql, $_SESSION['user']['id'], $_POST['pass']));
  • CSRF vulnerability. No form has been secured with a CSRF token.
  • Lack of protection against brute-force attacks. It just wasn’t there. No.

Here we could go on and on, but these problems are enough to understand: either the system had serious problems, or it itself was a serious problem.

3. The code was difficult to maintain and extend

Security issues were not limited to everything. To our surprise, the project lacked a version control system. The code was completely unstructured. The root directory of the web server contained files like ajax-new.php, ajax2.php, and they were all used in the code. There was also no clear delineation into layers (presentation, application, data). In the vast majority of cases, the code file was a mixture of PHP, HTML and JavaScript.

All this led to the fact that when we were asked to make a primitive backoffice for this system, the best solution was to deploy Symfony 4 side by side in conjunction with Sonata Admin and not touch the existing code at all. It is clear that if we were asked to add new opportunities for doctors or patients, it would take us a lot of time and energy. And since there was no talk of automatic tests, the likelihood of breaking something would be extremely high.

All of the above was enough for the government of Luxembourg – we were given the green light to develop a new platform.

The Doctor Rides-Rides: How We Developed a New Platform

We began to prepare for the development of a new platform from the very beginning – even when we saw the brainchild of an old vendor. Therefore, when we were given the go-ahead to develop a new platform, we immediately started creating its MVP version. A team of four PHP and three front-end developers coped with this task in about three and a half weeks. All work was carried out on Symfony 5, and only video calls and chats were delegated – they were implemented using our G-Core Meet service. The backoffice for the old system also came in handy: we managed to adapt it to MVP in just a couple of days. As a result, the MVP version of the system covered 80% of the functionality of the old platform. Now, by the way, it is also used for one more task – at one point we cloned the MVP for the helpdesk of the Luxembourg e-health agency, so that administrators there could call users.

When the MVP was ready, we started developing a full-fledged new platform. API Platform and ReactJS in conjunction with Next.js for client-side were used as the basis for the API. Not without interesting tasks.

1. Implementing notifications

One of the difficulties arose with notifications. Since API clients could be both mobile applications and our SPA, a combined solution was required.
First, we chose the Mercure Hub, with which customers interact through SSE (Server Sent Event). But no matter how the creators of the API Platform themselves promoted this solution, our mobile team rejected it, since the application could receive notifications with it only in an active state.

That’s how we came to Firebase, with which we managed to achieve support for native push notifications on mobile devices, and left the Mercure Hub for browser applications. Now, when an event occurred in the system, we notify the user about it through the private channel we need in the Mercure Hub and, in addition, sent a push to Firebase for a mobile device.

Why didn’t we immediately implement everything on Firebase? Everything is simple here: despite the support of web clients, browsers without the Push API – like Safari and most mobile browsers – do not work with it. However, we are still planning to implement push notifications from Firebase for those users using supported browsers.

2. Functional tests

Another interesting situation arose when we were doing functional tests for the API. As you know, each of them should work in a clean environment. But each time it turned out to be expensive in terms of performance to raise the base and fill in the basic fixtures + fixtures necessary for testing. To avoid this, we decided at the initial stage to raise the database based on the entity mapping (bin/console doctrine:schema:create) and only then add basic fixtures (bin/console doctrine:fixtures:load).

Then, using the dama / doctrine-test-bundle extension, we ensured that the execution of each test is wrapped in a transaction and at the end of the test case rolls it back without committing. Due to this, even if changes are made to the database during testing, they are not committed, and the database after the run remains in the same state as before PHPUnit was launched.
So that at least one test is written for each endpoint, we made an auto review test. It detects all registered routes and checks for tests for them. So, for example, for the app_appointment_create route, it checks if the folder contains tests/Functional/App/Appointment файл CreateTest.php

Additionally, the quality of the code is monitored by PHP-CS-Fixer, php-cpd and PHPStan with extensions like phpstan-strict-rules.

3. Client side

For doctors and patients, we have created two independent client applications with the same UI and similar capabilities. In order to establish the reuse of functionality and UI in them, we decided to use a mono-repository, which includes libraries and applications. At the same time, the applications themselves are built and deployed independently of each other, but may depend on the same libraries.

This approach allows you to create a feature (library) in one pull request and integrate it into all the applications you need. This advantage led to the use of a monorepository instead of implementing features in separate npm libraries and projects in different repositories.
To configure the monorepository, the ns.js library is used, which from the box allows you to build libraries and applications for React and Next.js, and it is this stack that is used in the project. We use ESLint and Prettier to keep track of code quality, Jest to write unit tests, and React Testing Library to test React components.

The doctor arrived: what happened in the end

In just five months, all problems were resolved, and the new platform became available to users of any device: we prepared a web version of the service, as well as mobile applications for iOS and Android.

For 4 months now, the service has been allowing patients to receive online consultations from doctors and dentists. They can take place in both audio and video formats. As a result, doctors write prescriptions and safely share medical records and test results with patients.

The platform is available to all healthcare professionals, residents and workers in Luxembourg. Now she works in 2 of the largest healthcare institutions in the country – in the Hospital. Robert Schuman (Hôpitaux Robert Schuman) and the Hospital Center. Emile Mayrisch (Center Hospitalier Emile Mayrisch). The service is deployed in a secure point of presence of the G-Core Labs cloud in the Luxembourg EDH Tier IV data center, where a virtual environment is configured for it in accordance with the required specifications.

Similar Posts

Leave a Reply