Secure web server
Added on 2019-01-13 15:56:00 UTC
For Security class we had to make a secured web server from scratch, with a few simple web pages and some user interaction.
The SecureWebServer is a .NET server application that uses TCP sockets to listen for new HTTP connections and handles these to show the proper page or process any of the user's interactions. Several measures have been taken to make the server as secure as possible.
Table of contents
Users can navigate to the server using any browser. They will first arrive on the homepage where they can choose to login if they want to. This gives them access to more of the server's functions, depending on their permissions.
From the homepage, the user can navigate to several other pages that the server offers. There is a page that gives an overview of the server's configuration and gives you the option to change it. From here, you can also see or erase the log file where all requests and responses are logged.
While no new users can be added to the server, it is possible to edit existing ones and their permissions.
It's also possible to toggle directory browsing, which allows users to navigate through the server's folder structure.
When the server starts, it loads a JSON configuration file. This contains the following settings:
- The port number that the server uses to listen for new connections;
- The root folder where all HTML pages and other files are present;
- A list of default pages the server should look for to use as the homepage;
- Whether the user is allowed to navigate through the server's folder structure or not.
Users can see and change the configuration, depending on their permissions. When changing the configuration, the new settings will be written to the JSON file. When the configuration changes, the server will automatically restart the RequestListener using the new configuration and navigates the user to the homepage. If the port has changes, the user will be navigated with the new port number instead.
The most important components of the application are described below.
The RequestListener listens for new HTTP requests on a separate thread, using TCP sockets. When a new request is made, the server will create a new thread where it will be handled. The network stream will be read and a new RequestMessage object will be created from it, containing all the data read from the HTTP request. The request will then be handled by the RequestHandler and when an exception occurs, the ErrorHandler will handle that.
The RequestHandler handles all HTTP requests. It uses the SecurityProvider to check if the user has access to the URL that was called, after which there will be searched for a CommandHandler that should be responsible for handling this specific URL. When no CommandHandler can be found, the URL will be treated as a path to a file or directory on the server. Eventually the RequestHandler will return a ResponseMessage object to the RequestListener so that the browser will get a proper HTTP response returned.
The SecurityProvider is responsible for the authentication and authorization of the user. This includes logging in and out and setting the session cookie after a succesful login attempt.
CommandHandlers have the responsibilty for handling URLs that do more than just serve static pages. The server knows 7 different CommandHandlers.
- IndexCommandHandler: This one is responsible for showing the homepage. Certain links are shown depending on whether the user is logged in and which permissions they have.
- ConfigCommandHandler: This handler shows the configuration of the server and allows users to change it if they have the correct permissions. After changing the configuration, the user will be redirected to the homepage using the new port if it has been changed.
- LogCommandHandler: This shows the contents of the log4net log file that contains an overview of all the requests and responses that were made. With the correct permissions, the user can also clear the log file.
- LoginCommandHandler: Handles the login attempt of the user.
- LogoutCommandHandler: Logs the user out.
- UserOverviewCommandHandler: Shows an overview of all the users that have access to the server.
- UserEditCommandHandler: Allows for changing an existing user and their permissions.
When an exception occurs and the RequestListener is able to catch it, it will then send it to the ErrorHandler. This will then handle two kinds of exceptions.
- RequestException: This is an exception that can be thrown anywhere in the code on purpose, allowing you to set an HTTP status code and an optional error message to show to the user.
- Any other kind of exception will cause an Internal Server Error with the status code (500) that does not show an error message to the user but a static page.
The ErrorHandler will make sure the user gets to see a static error page if one is present for the HTTP status code. If no page is present, the user will get to see a plain-text error message, if that was set.
Users are authenticated using a username and password. The server does not allow for new users to be created, but users that are already present in the SQL database can login.
Passwords are stored as SHA-512 hashes where a unique salt has been used.
After succesful authentication, the user and their permissions are cached in-memory using a sliding expiration of 20 minutes. A token is generated for this session and set in a cookie in the response headers.
Several URLs or actions are not available for users unless they have the proper permissions. For this, the server works with 2 kinds of user roles.
- "Beheerders" (administrators): These users can see and edit the server configuration. They can also inspect and clear the log file. Finally, they are also allowed to edit users and their permissions.
- "Ondersteuners"(support): These users can also see the server configuration but cannot change it. The same goes for the log file, which they can see but not clear. The user overview and edit pages are not available at all.
Which permissions are required for which URLs is configured in the server's app.config file.
Several measure have been taken to secure the server. This means that for the most part we had to follow the best practices of the OWASP Top 10 (2013). Because of time restrictions and other limitations not all security measure could be taken.
Implemented security measures
- SQL injection prevention: Variables in SQL queries are passed as SQL parameters. This means these values are not directly appended to the SQL query iteself, which prevents SQL injection.
- Password hashing: Passwords are stored as SHA-512 hashes where a unique salt has been used each time to make sure the hash is unique as well.
- Session tokens as cookies: Session tokens are not passed in URLs but stored in a cookie. Because the cookie has no expiration, browsers know they are dealing with a session cookie and will clear it once the browser closes. On the server's side, sessions are removed if there was no activity for over 20 minutes.
- User roles: Some URLs and actions require certain permissions. The user's permissions are checked by making sure the user is in the correct role.
- No direct object references: There are no direct references to resources such as IDs in the database.
- No stacktraces in error messaged: When errors occur, a static page is shown or a message that was deliberately set to be shown to the user. The stacktrace of an exception is never shown to the user when, for example, an Internal Sever Error (status code 500) occurs.
- Cross-Site Scripting prevention: Cross-Site Scripting (XSS) is prevented by validating user input to see if the format of the input is correct. Also, the Content-Security-Policy and X-XSS-Protection headers are always set for each response.
Not implemented security measures
- HTTPS: Because of the complexity and that the server only had to run locally, HTTPS was not implemented.
- Cross-Site Request Forgery prevention: Because of time limitations, Cross-Site Request Forgery (CSRF) prevention was not implemented.