Serving static files is a natural task for web servers. They, especially ones, having asynchronous architecture (like Nginx), are very good at such tasks.
However, usually there is an additional security logic, which should restrict access to files you've published. IIS, for example, offers deep integration with application layer, which allows custom .NET "middleware" logic injection into the request pipeline.
Node.js applications are very often published behind Nginx for various reasons and, with the help of Nginx "Secure Link" module, it's possible to offload static file serving tasks from node.js to Nginx, even if the files are not public. This module uses "shared secret" string (known to Nginx and the application) and expects a hash, based on this secret, to be present in the request to decide whether to proceed or return an error.
Secure Link module may work in 2 alternative modes (http://nginx.org/en/docs/http/ngx_http_secure_link_module.html):
It expects requests like /img/file.doc?h=[hash]&e=[expire time]. Lines in the configuration should be read as follows:
(1) When accessing /img/.. links, Nginx will first perform the following checks
However, usually there is an additional security logic, which should restrict access to files you've published. IIS, for example, offers deep integration with application layer, which allows custom .NET "middleware" logic injection into the request pipeline.
Node.js applications are very often published behind Nginx for various reasons and, with the help of Nginx "Secure Link" module, it's possible to offload static file serving tasks from node.js to Nginx, even if the files are not public. This module uses "shared secret" string (known to Nginx and the application) and expects a hash, based on this secret, to be present in the request to decide whether to proceed or return an error.
Secure Link module may work in 2 alternative modes (http://nginx.org/en/docs/http/ngx_http_secure_link_module.html):
- Simpler mode, based on "secure_link_secret" directive. Hash value in the request is based on concatenation of link to file and the secret. In this mode we can only check whether client was given hash for particular link
- More complex mode, based on 2 directives ("secure_link" and "secure_link_md5"). In this mode, we can restrict validity time of the hash and some other client-specific parameters, known to Nginx (like IP address or a header value)
This article focuses on the second mode, since it is more powerful and therefore more useful. Following is an example of Nginx configuration, using this mode:
It expects requests like /img/file.doc?h=[hash]&e=[expire time]. Lines in the configuration should be read as follows:
(1) When accessing /img/.. links, Nginx will first perform the following checks
(2) "secure_link" directive describes the way Nginx should extract generated hash value and (optionally) link expire time from the request. In this example, we specify that hash value and link expire time should be extracted from query string arguments "h" and "e" respectively.
(3)"secure_link_md5" directive describes what hash value Nginx expects to see in the request. "$secure_link_expires" variable contains expire time extracted from the request (value of "e" argument in our case). So, to generate correct hash value, we should hash concatenation of link expire time (seconds since "Epoch time"), request uri, space and "shared secret" ("6q3R9jhzG5" in our case)
(5-7) if hash comparison fails, return 404 error status
(9-11) if hash comparison succeeds, but expire time is less than server time, return 404 error status
(13) otherwise, continue with the request
Before browser (or other user agent) can request a file from Nginx, application server needs to generate a hash to be passed to the web server (along with expire time, used in hash generation). Following is a node.js function which generates such hash:
It's important to note that generated hash is expected to be in base64url format, which is ensured by the last line of the function.
Conclusion
Nginx secure link module may not have the best documentation, but once you understand it, it's very easy to use. I have found it useful, because it allowed to significantly decrease number of requests to node.js server and database hits.
MD5 algorithm has a bad reputation in security and, of course, its a bad idea to use it to store passwords. However, if you have long-enough secret (which should be longer, than in this example), it is absolutely impractical to use any of primitive techniques (like brute force or rainbow tables) to find secret out of hash value. Collision attack is not relevant either, since user agent does not present a secret to be hashed, but hash value itself. That means, that using md5 is a reasonably secure solution for secure links. Also, its performance is very important for this module to be useful.
Comments