The SIMS Portal is built on the Flask framework which comes with a number of built-in security features. The Flask-Login package, for example, offers a number of protections around user information when they sign in and out of the Portal, and cross-site request forgery (CSRF) protections ensure that actions users take with forms (e.g. editing their profile) are protected from bad actors.
In order to further improve security and performance, a number of headers have been added to the Portal on top of Flask. These offer a number of advantages, but require additional considerations when developing new features for this project. This guide walks through what they do in the current implementation, and offers a number of steps developers must take when changing certain elements inside the Portal.
What is a web application’s header?
A header in the context of a web application is the metadata included as part of the HTTP request and response cycle. These headers can contain different information based on the configuration of the site, such as caching policies, security settings, and more.
A request header is sent by the client (the person looking at the site) to the server, such as the user’s operating system, what type of data the user’s machine is willing to accept back, cookie information, and more. A response header is what is sent back by the server and might include things like what type of data is being sent to the client, how long to cache certain data, and various elements designed to prevent third party injection of malicious code.
Here’s an example of what an HTTP response header might look like:
HTTP/1.1 200 OK
Date: Wed, 19 Jun 2024 12:00:00 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 1387
Connection: keep-alive
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
Set-Cookie: sessionId=abcd1234; Path=/; HttpOnly
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'; img-src 'self'; script-src 'self'; style-src 'self'
Understanding the SIMS Portal header implementation
There are a number of ways that headers could be specified given the SIMS Portal’s configuration. As it runs on AWS, we could use CloudFront to inject the headers each time a user loads the site. But in order to make the setup more straightforward, I’ve opted to build this directly into the application factory within the __init__.py
file. Here’s what that looks like:
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
migrate.init_app(app, db)
# send build_ns_dropdown() data to context_processor for use in layout.html
app.context_processor(build_ns_dropdown)
bcrypt.init_app(app)
login_manager.init_app(app)
admin = Admin(app, name='SIMS Admin Portal', template_mode='bootstrap4', endpoint='admin')
babel = Babel(app)
Markdown(app)
cache.init_app(app)
csrf = CSRFProtect(app)
@app.after_request
def add_caching_headers(response):
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"img-src 'self' data: https://www.google.com https://www.gstatic.com; "
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://d3js.org https://unpkg.com https://www.google.com https://www.googletagmanager.com https://cdnjs.cloudflare.com https://ajax.googleapis.com https://maxcdn.bootstrapcdn.com https://cdn.datatables.net https://www.gstatic.com; "
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://cdn.datatables.net https://maxcdn.bootstrapcdn.com; "
"font-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net https://maxcdn.bootstrapcdn.com data:; "
"connect-src 'self' https://www.google-analytics.com https://www.gstatic.com"
)
return response
...
What we’re focused on here is the add_caching_headers()
function. The line above it is a decorator which tells Flask to run this function after every request is processed but before the response is sent back to the user’s browser. To illustrate what this means, it can help to use an analogy—Imagine the request/response cycle as a customer (the user) walking up to a counter at a store and asking the employee to get something out of the store room, that’s the request. The employee turns around and goes into the store room (the server) and finds what the customer requested. When they go back to the counter to hand it over, that’s the response. In this case, the add_caching_headers()
with the .after_request
decorator is just telling the server to stick some metadata on top of the item that the employee is giving to the customer, like printing out a manual and putting it on top of the box when they hand it over.
Walk-through of current headers
This guide was drafted as of version 1.11.4, so the code you see today may look slightly different if new features have been added since. Let’s walk-through what each header is doing as of the version you see above.
Cache-control headers
no-store
: Ensures that sensitive information is not stored in the browser cache or any intermediate caches.no-cache
: Requires the browser to re-validate the content with the server before using a cached version.must-revalidate
: Directs caches to re-validate the content with the origin server before serving a cached copy.max-age=0
: Indicates that the content is stale immediately and must be re-validated before use.
Security headers
- X-Content-Type-Options
nosniff
: Prevents browsers from MIME-sniffing a response away from the declared content-type, which can prevent certain types of attacks based on content-type mismatches.
- Strict-Transport-Security (HSTS)
max-age=31536000
: Enforces HTTPS for one year.includeSubDomains
: Ensures that all subdomains must also use HTTPS
- X-Frame-Options
DENY
: Prevents the site from being framed by another site. This mitigates clickjacking attacks, where an attacker tricks a user into clicking something different from what the user perceives.
Content-Security-Policy
- Content-Security-Policy:
default-src 'self'
: Sets a default policy that content can only be loaded from the same origin.img-src 'self' data
: https://www.google.com https://www.gstatic.com: Restricts image sources to the same origin, data URIs, and specified trusted domains.script-src 'self' 'unsafe-inline' (…)
: Limits script sources to the same origin, inline scripts, and specified trusted domains.style-src 'self' 'unsafe-inline' (…)
: Limits style sources similarly to scripts, allowing inline styles.font-src 'self' (…)
: Restricts font sources to the same origin and specified trusted domains.connect-src 'self' (…)
: Restricts the sources for AJAX calls and other connections to the same origin and specified trusted domains.
When to update this function
It is unlikely that any future developers would need to touch the cache-control or security headers, but if they do, there probably won’t be any impact on the rest of the application. Testing any changes both locally and in a staging environment are still encouraged.
Where SIMS Portal developers will need to make changes will more likely be around the content-security-policy—specifically, the script-src
line. The SIMS Portal, like many modern web apps, imports a number of libraries when pages are loading. These libraries allow us to build visualizations with D3 and make use of the Bootstrap framework for CSS. We prevent the malicious injection of other libraries that could affect performance and security by whitelisting the content delivery networks (CDNs) that the application can interact with. If you decide to add a feature that relies on the importing of another script, you need to add it here. For example, if you decide to swap out DataTables (the library that helps produce filterable, searchable tables in the site) with another service, you would remove https://cdn.datatables.net
from this line and add the new one.