Preface
I think many will be interested in an article about the security of web projects, so the time has come. I will often refer to the architecture I use, but the approaches described can be used in any system (the main thing is to know how).
Security levels
Many programmers, thinking about system security, imagine a single, universal mechanism that allows protecting the entire system by calling (for example) the toDoWell function or the $security->work() method, but unfortunately, this approach does not justify itself; the result is a lump dirty code that does not protect the system at all. This kind of problem has the same roots as all the lumps of dirty code in programming - the divine object. This antipattern says that one mechanism should not be used to solve unrelated problems, even if the word security is present in all tasks.
Let's first divide the possible dangers that threaten the system into parts:
1. Hardware failure - failure of servers, communication lines, DDoS, etc. No script will help you here; you need to either configure the server correctly or contact the administrators for help. We will not talk about this problem in this article;
2. Data substitution - lack of verification (filtering) of data received from the user, which often leads to xss, sql inj and other unpleasant problems;
3. Access - the user obtaining the right to perform an operation that he is not allowed to perform, for example, changing the password of another user;
4. Visibility - this type of danger is the least dangerous; it rather serves to improve the user’s perception of information. This includes showing different information to different users, for example, the administrator sees system control levers, and the user only sees a simplified interface without unnecessary buttons.
The last three problems can and should be solved using software. Start over.
Availability
If we look at the client-server system (web site) as a client-server system, we will notice that there are two mechanisms involved: the client - which transmits data; and the server - which receives and processes this data. Let's call the data transferred from the client to the server a command. Thus, the addUser Baskka 123 command sent to the server allows you to create a new user with the Bashka login and password 123, and the removeUser Bashka command allows you to delete this user. The name of the command and the parameters passed with it will be called the semantics of the command. Knowing the semantics of a command, we can determine the data that this command should accept and the data that this command should not accept. For example, the addUser command must take the user's login in the first parameter, and it is necessary to determine in advance which characters can be included in the login and which cannot. For example, we will prohibit users from registering accounts with characters other than A-Za-z_0-9. The fewer characters users can use (especially system characters), the more secure the system will be. Try inserting sql inj or xss using only A-Za-z0-9
Let's call the available character set of a parameter its verification mask. Verification is the process of checking data for compliance with the verification mask. If the data contains characters that are not in the verification mask, verification is considered failed.
Let's combine what we received:
1. Command - access to the server;
2. Command parameters - data transmitted to the server;
3. Parameter verification mask - characters that can be included in the parameter;
4. Parameter verification - checking the parameter for the presence of characters that are not in the verification mask.
Let's write the command in a more expanded form:
Login = [A-Za-z_0-9]
Pass = [A-Za-z_0-9*?]
addUser Login Pass
It can be seen that the command accepts only certain data. If we try to pass data of an invalid type into it, the system will return an error without harming itself.
How to implement this approach in practice? In my case, everything is implemented as follows:
There are data classes: Integer, String, Float, Alias, FileName, FileAddress, etc. Each class includes an isReestablish method, which accepts data and verifies it according to its mask. For example, the verification mask for Boolean is: true|false
Each module has its own controller, and the user can only call methods of these controllers. Each controller method has parameters (like any other class method), and these parameters are strictly typed, for example:
class Controller{
public function addUser(Login $login, Pass $pass){
...
}
}
When a user submits a request to the system, it goes to the central controller. The request contains the following information: module name, method name, parameters. The central controller finds the module the user needs, receives its controller, then receives the controller method and, going through all the parameters of the method, determines what type the data should correspond to. If during this check it turns out that the method expects some parameters, but the user passed others (by calling the isReestablish method), then the central controller returns an error.
Note that I don't need to manually check the incoming data via if and preg_match, everything is done automatically. I can also reuse verification masks in other controllers, rather than rewriting the verification code. Security is also taken into account, since the user will not be able to pass data that the module method does not expect. Mission Complete.
Access
Let's return to the client-server architecture. A kosher (readable) system most often has a modular (or batch) architecture, with each module responsible for its own tasks and processing its own commands. Obviously, not all users should have access to the modules available in the system (at least not to all module commands), so the question arises - how to deny a user access to a module?
There are several ways to restrict access and they all boil down to one thing - giving or denying access to one user for one operation. It looks something like this: Bashka !addUser,removeUser - the Bashka user is denied access to the addUser and removeUser operations. Of course, the process of restricting user access to commands will turn into a chore if you do not combine the rules into roles: User !addUser,removeUser; Admin - User roles do not have access to commands for creating and deleting users, but there are no prohibitions for the admin. Now you just need to assign certain roles to certain users and the job is done! If the user sends a command to the server, the server must check whether the server is allowed to process the command or whether it is prohibited.
How to implement this approach in practice? In my case, everything is implemented as follows:
There are Users and Access modules. The first is responsible for accounting for system users, the second for delineating rights. The Access module includes entities such as Role and Rule. The first defines the roles that can be assigned to the user, the second access rights - what the user cannot do (the name of the module and the name of the method that cannot be called by this user). So, if we create a Rule like module:Users,action:addUser and extend Role User to it, then all users with this role will not be able to call the addUser method of the Users module.
The central controller of the system (as mentioned earlier) receives from the user the name of the module and the method it accesses, but before passing a command to the module, the central controller checks whether the user is allowed access to this module (by checking its roles), if access is denied, an error is returned without harm to the server.
Visibility
What happens if a user opens a site and sees a “Customize” or “RemoveAllUser” button? I think he'll click on it! It doesn’t matter that the system won’t react to this in any way (the user isn’t given access to unnecessary module methods?!), the very presence of extra buttons is annoying. Therefore, it is necessary to implement a mechanism for hiding unnecessary interface components from the user. This is implemented in several ways, the simplest of which is to check which roles are defined for users and, depending on this, hide or show screen components. The user, of course, will be able to see prohibited components in a roundabout way, but this will not harm the system in any way, since at the server level, access to commands is denied to the user.
How to implement this approach in practice? Unfortunately, I won’t have time to describe it in detail, so suggest an option in the comments
Afterword
You can protect the system in many ways, but remember - each danger is a separate task that must be solved by its own mechanism.
Good luck!
I think many will be interested in an article about the security of web projects, so the time has come. I will often refer to the architecture I use, but the approaches described can be used in any system (the main thing is to know how).
Security levels
Many programmers, thinking about system security, imagine a single, universal mechanism that allows protecting the entire system by calling (for example) the toDoWell function or the $security->work() method, but unfortunately, this approach does not justify itself; the result is a lump dirty code that does not protect the system at all. This kind of problem has the same roots as all the lumps of dirty code in programming - the divine object. This antipattern says that one mechanism should not be used to solve unrelated problems, even if the word security is present in all tasks.
Let's first divide the possible dangers that threaten the system into parts:
1. Hardware failure - failure of servers, communication lines, DDoS, etc. No script will help you here; you need to either configure the server correctly or contact the administrators for help. We will not talk about this problem in this article;
2. Data substitution - lack of verification (filtering) of data received from the user, which often leads to xss, sql inj and other unpleasant problems;
3. Access - the user obtaining the right to perform an operation that he is not allowed to perform, for example, changing the password of another user;
4. Visibility - this type of danger is the least dangerous; it rather serves to improve the user’s perception of information. This includes showing different information to different users, for example, the administrator sees system control levers, and the user only sees a simplified interface without unnecessary buttons.
The last three problems can and should be solved using software. Start over.
Availability
If we look at the client-server system (web site) as a client-server system, we will notice that there are two mechanisms involved: the client - which transmits data; and the server - which receives and processes this data. Let's call the data transferred from the client to the server a command. Thus, the addUser Baskka 123 command sent to the server allows you to create a new user with the Bashka login and password 123, and the removeUser Bashka command allows you to delete this user. The name of the command and the parameters passed with it will be called the semantics of the command. Knowing the semantics of a command, we can determine the data that this command should accept and the data that this command should not accept. For example, the addUser command must take the user's login in the first parameter, and it is necessary to determine in advance which characters can be included in the login and which cannot. For example, we will prohibit users from registering accounts with characters other than A-Za-z_0-9. The fewer characters users can use (especially system characters), the more secure the system will be. Try inserting sql inj or xss using only A-Za-z0-9
Let's combine what we received:
1. Command - access to the server;
2. Command parameters - data transmitted to the server;
3. Parameter verification mask - characters that can be included in the parameter;
4. Parameter verification - checking the parameter for the presence of characters that are not in the verification mask.
Let's write the command in a more expanded form:
Login = [A-Za-z_0-9]
Pass = [A-Za-z_0-9*?]
addUser Login Pass
It can be seen that the command accepts only certain data. If we try to pass data of an invalid type into it, the system will return an error without harming itself.
How to implement this approach in practice? In my case, everything is implemented as follows:
There are data classes: Integer, String, Float, Alias, FileName, FileAddress, etc. Each class includes an isReestablish method, which accepts data and verifies it according to its mask. For example, the verification mask for Boolean is: true|false
Each module has its own controller, and the user can only call methods of these controllers. Each controller method has parameters (like any other class method), and these parameters are strictly typed, for example:
class Controller{
public function addUser(Login $login, Pass $pass){
...
}
}
When a user submits a request to the system, it goes to the central controller. The request contains the following information: module name, method name, parameters. The central controller finds the module the user needs, receives its controller, then receives the controller method and, going through all the parameters of the method, determines what type the data should correspond to. If during this check it turns out that the method expects some parameters, but the user passed others (by calling the isReestablish method), then the central controller returns an error.
Note that I don't need to manually check the incoming data via if and preg_match, everything is done automatically. I can also reuse verification masks in other controllers, rather than rewriting the verification code. Security is also taken into account, since the user will not be able to pass data that the module method does not expect. Mission Complete.
Access
Let's return to the client-server architecture. A kosher (readable) system most often has a modular (or batch) architecture, with each module responsible for its own tasks and processing its own commands. Obviously, not all users should have access to the modules available in the system (at least not to all module commands), so the question arises - how to deny a user access to a module?
There are several ways to restrict access and they all boil down to one thing - giving or denying access to one user for one operation. It looks something like this: Bashka !addUser,removeUser - the Bashka user is denied access to the addUser and removeUser operations. Of course, the process of restricting user access to commands will turn into a chore if you do not combine the rules into roles: User !addUser,removeUser; Admin - User roles do not have access to commands for creating and deleting users, but there are no prohibitions for the admin. Now you just need to assign certain roles to certain users and the job is done! If the user sends a command to the server, the server must check whether the server is allowed to process the command or whether it is prohibited.
How to implement this approach in practice? In my case, everything is implemented as follows:
There are Users and Access modules. The first is responsible for accounting for system users, the second for delineating rights. The Access module includes entities such as Role and Rule. The first defines the roles that can be assigned to the user, the second access rights - what the user cannot do (the name of the module and the name of the method that cannot be called by this user). So, if we create a Rule like module:Users,action:addUser and extend Role User to it, then all users with this role will not be able to call the addUser method of the Users module.
The central controller of the system (as mentioned earlier) receives from the user the name of the module and the method it accesses, but before passing a command to the module, the central controller checks whether the user is allowed access to this module (by checking its roles), if access is denied, an error is returned without harm to the server.
Visibility
What happens if a user opens a site and sees a “Customize” or “RemoveAllUser” button? I think he'll click on it! It doesn’t matter that the system won’t react to this in any way (the user isn’t given access to unnecessary module methods?!), the very presence of extra buttons is annoying. Therefore, it is necessary to implement a mechanism for hiding unnecessary interface components from the user. This is implemented in several ways, the simplest of which is to check which roles are defined for users and, depending on this, hide or show screen components. The user, of course, will be able to see prohibited components in a roundabout way, but this will not harm the system in any way, since at the server level, access to commands is denied to the user.
How to implement this approach in practice? Unfortunately, I won’t have time to describe it in detail, so suggest an option in the comments
Afterword
You can protect the system in many ways, but remember - each danger is a separate task that must be solved by its own mechanism.
Good luck!