Monitoring and Security

Secure Design and Coding


Learning Objectives

  • You know of the threats that web applications face and know the questions that should be repeatedly asked during threat modeling.
  • You know of principle of least privilege, defence in depth, and attack surface minimization.
  • You understand that secure design is a continuous process that continues also after the application has been built.

Threat landscape

Web applications are continuously scanned for vulnerabilities. Much of this scanning is automatic, done by bots that crawl the internet looking for known weaknesses such as SQL injection, cross-site scripting (XSS), and cross-site request forgery (CSRF). Non-application specific vulnerabilities, such as open ports, are also targeted.

These topics are also briefly discussed in the Users and Security part of the Web Software Development course.

In addition to the traditional threats, problems also come from third-party integrations and supply chain vulnerabilities. Vulnerabilities in third-party libraries and services can introduce vulnerabilities also to an otherwise secure system. A malicious actor could compromise a widely used library, which would then be included in many applications, leading to a widespread security incident.

As an example, the npm package event-stream had an incident where one of its dependency was compromised. Everyone who updated the event-stream package at a certain point in time had also the compromised dependency downloaded, which then attempted to steal BitCoins from BitCoin wallets on the developers machines. The flow of this attack is illustrated in Figure 1 below.

Figure 1 — Flow of the event-stream attack. At the beginning, everything was ok and there were no problems with the event-stream package. However, when the malicious code was introduced to the third-party dependency ad the event-stream package updated, BitCoins were stolen.

In the case of the event-stream attack, the target was the developers’ machines. However, the same principle could be applied to any kind of applications. As an example, one of the SolarWinds supply chain attack led to malware being distributed to thousands of organizations.

Loading Exercise...

Secure design

Secure design is a practice that focuses on planning and developing systems in a manner that possible attacks are considered already during design and taken into account during development. By integrating security into the development process early on, it is possible to avoid many vulnerabilities from the start.

Security should not be something that is glued to an application afterwards. Instead, security should be continuously considered when building applications.

Threat modeling is a key part of secure design. Threat modeling is a structured approach for identifying and mitigating security risks. There are a few threat modeling methodologies, like STRIDE, that focus on identifying potential scenarios.

By identifying potential scenarios with security risks, developers can design and create countermeasures even before the code is written. The threat modeling manifesto is also a good starting point for threat modeling — the manifesto highlights four key questions that should be iterated over:

  1. What are we building?
  2. What can go wrong?
  3. What are we going to do about it?
  4. Did we do a good enough job?

When designing countermeasures, risk assessment is used to evaluate the potential impact and likelihood of the identified vulnerabilities. This helps prioritizing which vulnerabilities to address, for example, based on the potential damage. Threat modeling and risk assessment are used together to inform secure architectural decisions.

There are also open-source tools like OWASP Threat Dragon that can help with threat modeling.

Loading Exercise...

Security principles

There are a handful of principles used in secure design. Defence in depth uses layers of security measures so that even if one fails, others continue to protect the system. For example, for data access, an application could have a firewall, authentication and authorization, and data encryption. Even if the firewall and authentication and authorization would be breached, the data would still be encrypted. These layers are visualized in Figure 2.

Figure 2 — Defence in depth uses layers of security so that even if one of the layers fails, others continue to protect the system.

Another principle is the principle of least privilege. This principle dictates that every process, user, or system component should have only the minimal access necessary to perform its function. This minimizes the potential damage that could occur if a component is compromised, as attackers will have access only to limited areas of the system.

As an example, when setting setting up the connection to the database when studying database data in the previous chapter, we did not follow the principle of least privilege. Instead of having or creating a user with only read rights to specific tables, we used a user that has full access to the database. If a malicious actor would gain access to the Grafana dashboard, they would have full access to the database, and could easily cause damage.

Concretely, the principle of least privilege can be applied by limiting the permissions granted to users, processes, and systems. For example, when running a Deno application, the --allow-* flags should be set to the minimum required permissions — if an application requires read access to a specific folder, it should only be granted read access to that folder, not to the whole file system (i.e., instead of --allow-read, using --allow-read=/path/to/folder).

For additional information on Deno’s security see Security and permissions in Deno documentation.

Similarly, users should be granted access only to the resources they need to perform their tasks. An example of this principle in action is a Role-Based Access Control (RBAC) system, where users are assigned roles with specific permissions.

const roles = {
  admin: ["read", "write", "delete"],
  user: ["read"]
};

function checkPermission(role, action) {
  return roles[role] && roles[role].includes(action));
}

console.log("Admin delete? ", checkPermission("admin", "delete"));
console.log("User delete? ", checkPermission("user", "delete"));

A third principle is attack surface minimization. This principle involves reducing the number of ways an attacker can interact with a system. By limiting the attack surface, developers can reduce the likelihood of successful attacks. This can be achieved by disabling unnecessary services, closing unused ports, and regularly auditing configurations.

A good starting point for web applications is using a load balancer that acts as the entry point to the application. This way, the load balancer can be configured to only allow access to the necessary ports, and the application itself can be isolated from the internet.

That is, database ports, for example, should not be exposed to the world — instead, the database should be accessed through the application, which is accessed through the load balancer.

Loading Exercise...

Secure coding

Coding securely is a continuous process that requires an understanding of what can go wrong and how to prevent it. Secure coding practices involve validating inputs, sanitizing outputs, and following best practices that are applicable across technologies. Often, it is also better to use widely-used libraries for a common task instead of rolling out a custom solution.

As an example, we’ve used Zod for validation, Scrypt for password hashing, Hono as the web framework, and so on. These libraries are widely used and have been tested by many developers.

Secure configuration management is another practice that is needed for secure coding. In this course, we’ve kept environment variables in a .env file, which is a good practice. However, it is also important to keep the .env file out of the repository, as it can contain sensitive information.

There are also tools that can help with secure configuration management, like Vault.

As web applications increasingly use APIs and third-party libraries, possible problems from APIs should be kept in mind. APIs can be a source of security vulnerabilities — as an example, if an API is compromised and data from the API is shown to a user, the user could be tricked into revealing sensitive information. Similarly, some sort of a mechanism for rate limiting and throttling should be in place to prevent brute force and denial-of-service attacks, and logging and monitoring should — of course — be in place to detect and respond to potential abuse.

As mentioned earlier, the logs should be collected to a separate service, and not stored in the same server as the application. This way, even if the application is compromised, the logs are still available for analysis.

Similarly, when working with third-party libraries, it is important to keep in mind the risks associated with them. Keeping track of the user third-party libraries and regularly auditing and updating them is important. Monitoring for reported vulnerabilities is also important to prevent supply chain attacks.

Sites like Snyk Vulnerability Database can be used to look for vulnerabilities in libraries. There are also a range of tools that can be used to scan applications — see e.g. Vulnerability Scanning Tools list maintained by OWASP.

Runtimes often also come with tools that can be used to check whether the dependencies are up to date or secure. As an example, Deno has a command outdated that can be used to check for outdated dependencies. Similarly, npm has a command audit that can be used to check for vulnerabilities in the dependencies.

Loading Exercise...

Testing, deployment, and maintenance

Security should be a part of the whole software development lifecycle, including also testing, deployment, and maintenance. Even if no known vulnerabilities are present in the code or the libraries at the time of design and development, it is possible that new vulnerabilities are discovered over time, or that vulnerabilities are introduced during deployment or maintenance.

As an example, a misconfiguration in the deployment could lead to a security vulnerability, or a dependency could be updated to a version that has a vulnerability.

Integrating DevSecOps practices can to the development process can be help. DevSecOps practices include automated security scanning, continuous integration of security tools, and rapid feedback loops. As an example, version control systems can have hooks that trigger security scans whenever code is pushed to the repository or whenever code is being built.

Tools like GitHub’s CodeQL should be integrated to the development process. Similarly, the repositories should be continuously monitored with tools like Dependabot. With more people using large language models and generative AI for development, this is even more critical, as the models are known to create code with vulnerabilities.

Loading Exercise...

Security and Cloud Computing

The above provides just a glimpse of the security principles and secure coding practices. For a more in-depth view to the topic, Aalto University has a Master’s Programme in Security and Cloud Computing.