Small security checklist for public backend services

Posted on 2021-09-19 in Trucs et astuces

Here are some security tips to check for backend services. It's mostly meant so that I can have a check list. So I don't develop them much but provide extra links where necessary. I also probably expand this list as time goes one and I learn more about this subject.

HTTP headers

  • Make sure your server doesn't respond with its version (and perhaps even its name): with this information, an attacker knows which security holes to use.
    • For example, with nginx it's done with server_tokens off;.
    • Also make sure your framework don't leak this information in cookies.
  • Enable content security policy to mitigate XSS and data injection attacks.
    • In SPA (with React for instance), you can inject this policy in the HTML instead of relying on a header. It's mostly useful if some CSS or JS must be inline in the HTML. This webpack plugin will help you acheive that and limit inlined CSS and JS to code that matches a hash created at generation time so you are still protected against XSS.
    • If you use create-react-app, you can set INLINE_RUNTIME_CHUNK to false to disable inline script and use HTTP headers correctly. See the documentation.
    • If you need to add inline scripts outside your build system, you can enable CSP and load the page of Chrome to get the hash of the script to use in your CSP policy.
    • You can use either frame-ancestors 'none' with CSP or X-Frame-Options: deny. See here.
  • Enable strict transport security so you browser always connect to your site with HTTPS (after the first visit).
    • With nginx it's done with something like add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;.
  • Extra headers:
    • Configure a Referer Policy.
    • Configure a Permissions Policy
    • Other useful headers, mostly to prevent user tracking: add_header Permissions-Policy "interest-cohort=()" always;, Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Resource-Policy: same-site and Cross-Origin-Embedder-Policy: unsafe-none. See this article for the rational being this.

Astuce

You can use this tool to check your HTTP headers.

Cookies

  • Authentication cookies must be created with httpOnly=true and secure=true so they cannot be accesed by javascript (in case of an XSS attack) nor intercepted. They should also probably be restricted to a specific domain and expire in a resonnable amount of time. - You must enable CSRF protection on all views that performs actions for autheniticated user (see here for Django). If you rely on a cookie you can set httpOnly=true if you need to use it with AJAX request (Django's documentation explains this well). - You must also configure SameSite for your cookies, Lax being a good default in most cases. See here for more details.
  • Beware of cookie and CNAME cloaking (even CNIL, the French data protection authority talks about it): the idea is that some advertising company will ask you to add a CNAME entry to your DNS (eg tracking.jujens.eu) so they can track your user (this technique is hard to detect and block by tools since it can be a legitimate domain). Here is the security risk: if your authentication cookies are sent to all you subdomains, you will send them to the advertising server, allowing them to connect as your users. You can disallow this technique (not always possible) or mitigate this by restricting to which domains your cookies are sent or switching to another authentication method (like tokens [1]).

Misc

  • Add audit for user actions (eg when user connects, changes password) if required.
  • Enable Subresource integrity so your browser can verify the resource it fetches are feched without manipulations.
  • Never enable your frameworks debug mode in production: this will make you leak sensitive information like configuration.
  • Make your secure your connection with the database. For Django, you should at least set sslmode to prefer and ideally configure full certificate validation. See here.
  • Don't store credit card data (these data shouldn't even be seen by your system).
  • Validate and sanitize all incoming data with forms. Escape it if necessary.
    • In Python, bleach is great for that.
  • Beware of XML: by default it contains feature that presents security risks.
    • In Python, defusedxml is a great parser to avoid that.
  • Allow users to enable 2FA and force admin to use it.
  • Obfuscate primary keys with UUIDs to resist enumeration attacks.
  • Don't store passwords in plain text: store your application password hashes instead. Add a random salt as well.
  • Don't log any sensitive data: filter out the confidential data, such as API keys, before recording them in your log files.
  • Any secure transaction or login should use SSL: be aware that eavesdroppers in the same network as you could listen to your web traffic if it is not in HTTPS. Ideally, you ought to use HTTPS for the entire site.
  • Avoid using redirects to user-supplied URLs: If you have redirects such as http://example.com/r?url=http://evil.com, then always check against whitelisted domains.
  • Check authorization even for authenticated users: Before performing any change with side effects, check whether the logged-in user is allowed to perform it.
  • Don't keep your backend code in web root: This can lead to an accidental leak of source code if it gets served as plain text.
  • Use templating libraries with XSS protection built in.
  • Use an ORM rather than SQL commands: good ORMs offers protection against SQL injection.
  • Use Django forms with POST input for any action with side effects: It might seem like overkill to use forms for a simple vote button, but do it.
  • CSRF should be enabled and used.
    • In Django, be very careful if you are exempting certain views using the @csrf_exempt decorator.
  • Ensure that Django and all packages are the latest versions. Plan for updates (tools like dependabot can help).
  • Limit the size and type of user-uploaded files.
  • Run https://www.qualys.com/forms/freescan/, https://www.owasp.org/index.php/OWASP_Dependency_Check and https://observatory.mozilla.org/ to detect potential issues.

Django

  • Never use Meta.exclude because you may include fields in a form by accident. For the same reason, don't use the __all__ shortcut in fields.
  • Django automatically escapes data dynamically inserted in templates. But you still need to quote all HTML attribute. For example, replace <a href={{link}}> with <a href="{{link}}">.
  • Avoid the extra and execute functions of the ORM. The ORM will prevent SQL injection by default but with these you need to do the work manually.
  • You can use secure.py to use standard security headers.
  • Change the default admin URL to something else so it is harder to find for an attacker.
  • Don't keep SECRET_KEY in version control. As a best practice, pick SECRET_KEY from the environment. Check out the django-environ package.

Examples

Django

This relies on django-cors-headers

from corsheaders.defaults import default_headers

# Security
# See https://github.com/adamchainz/django-cors-headers
CORS_ALLOW_HEADERS = default_headers
CORS_ORIGIN_WHITELIST = [FRONTEND_BASE_URL]
CORS_ALLOW_CREDENTIALS = True
# Both of these must be False so we can correctly use the CSRF token in
# our AJAX request.
# See: https://docs.djangoproject.com/en/3.1/ref/csrf/#ajax
# and https://www.django-rest-framework.org/topics/ajax-csrf-cors/
CSRF_USE_SESSIONS = False
CSRF_COOKIE_HTTPONLY = False
CSRF_TRUSTED_ORIGINS = env.tuple("CSRF_TRUSTED_ORIGINS", default=("localhost",))
CSRF_COOKIE_DOMAIN = env.str("CSRF_COOKIE_DOMAIN", "localhost")
# https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options
X_FRAME_OPTIONS = "DENY"
# https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff
SECURE_CONTENT_TYPE_NOSNIFF = True
# https://docs.djangoproject.com/fr/3.1/ref/settings/#password-reset-timeout
PASSWORD_RESET_TIMEOUT = 1 * 24 * 60 * 60
# https://docs.djangoproject.com/en/3.2/ref/middleware/#module-django.middleware.security
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
# CSP settings
# https://django-csp.readthedocs.io/en/latest/configuration.html
CSP_DEFAULT_SRC = ("'self'",)
CSP_CONNECT_SRC = ("'self'",)
# To use for JS, CSS, images and fonts.
CUSTOM_CSP_STATIC_SRC = env.tuple("CUSTOM_CSP_STATIC_SRC", default=("'self'",))
CSP_SCRIPT_SRC = CUSTOM_CSP_STATIC_SRC
CSP_IMG_SRC = CUSTOM_CSP_STATIC_SRC
CSP_MEDIA_SRC = CUSTOM_CSP_STATIC_SRC
CSP_FONT_SRC = CUSTOM_CSP_STATIC_SRC
CSP_STYLE_SRC = CUSTOM_CSP_STATIC_SRC
CSP_BLOCK_ALL_MIXED_CONTENT = True
CSP_UPGRADE_INSECURE_REQUESTS = True
# https://docs.djangoproject.com/en/3.2/ref/settings/#secure-browser-xss-filter
SECURE_BROWSER_XSS_FILTER = True
# Secure connection
SECURE_REDIRECT_EXEMPT = [r"/?health/?"]
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

NextJS

You can have the code below in your next.config.js:

const production = process.env.NODE_ENV === "production";

const getCsp = () => {
    let csp = ``;
    csp += `base-uri 'self';`;
    csp += `form-action 'self';`;
    csp += `default-src 'self';`;
    csp += `frame-src DOMAIN;`;
    // NextJS requires 'unsafe-eval' in dev (faster source maps)
    // sha256-XXXX is for XX service.
    csp += `script-src 'self' ${
        production ? "" : "'unsafe-eval'"
    } https://maps.googleapis.com 'sha256-XXX'`;
    // NextJS requires 'unsafe-inline'. Hash are not supported. Neither are nonce (the style tags are
    // not updated correctly. This can also be limited to our usage of MaterialUI.
    // Furthermore, since nonce must be generated at each request, we could get into issues with
    // caching for these public pages.
    csp += `style-src 'self' 'unsafe-inline' data: https://fonts.google.com https://fonts.googleapis.com https://client.crisp.chat;`;
    csp += `img-src 'self' data: blob: https://maps.googleapis.com https://maps.gstatic.com https://maps.googleapis.com https://storage.googleapis.com ${process.env.NEXT_PUBLIC_BACKEND_API_DOMAIN_URL};`;
    // noembed.com is required for ReactPlayer to work correctly.
    csp += `connect-src 'self' https://noembed.com wss://client.relay.crisp.chat;`;
    csp += `font-src 'self' https://fonts.gstatic.com https://client.crisp.chat;`;
    csp += `media-src 'self' https://storage.googleapis.com`;
    return csp;
};

const headers = [
    {
        key: "X-Content-Type-Options",
        value: "nosniff"
    },
    {
        key: "X-Frame-Options",
        value: "DENY"
    },
    {
        key: "X-XSS-Protection",
        value: "1; mode=block"
    },
    {
        key: "Content-Security-Policy",
        value: getCsp()
    }
];

let moduleExports = {
    async headers() {
        return [
        {
            source: "/(.*)",
            headers
        },
        {
            source: "/:path*",
            headers
        }
        ];
    },
};

nginx

I suppose that CSP in handled by the app itself (eg with Django's configuration).

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Permissions-Policy "interest-cohort=()" always;
add_header Cross-Origin-Opener-Policy  same-origin always;
add_header Cross-Origin-Resource-Policy same-site always;
add_header Cross-Origin-Embedder-Policy unsafe-none always;

csp-html-webpack-plugin

new cspHtmlWebpackPlugin(
    // We still need to have unsafe-inline to support old browser. Modern browser will just
    // ignore it if nonce or hash is set.
    {
    'default-src': ["'self'"],
    'connect-src': [
        `${apiUrl.host}`,
        '*.sentry.io',
        'o552216.ingest.sentry.io',
        'sentry.io',
    ],
    'style-src': ["'self'", "'unsafe-inline'", 'fonts.googleapis.com'],
    'font-src': ["'self'", 'fonts.gstatic.com'],
    'img-src': [
        "'self'",
        'data:',
        'maps.gstatic.com',
        'storage.googleapis.com',
        `${apiUrl.host}`,
    ],
    'script-src': [
        "'unsafe-inline'",
        "'self'",
        "'unsafe-eval'",
        'o552216.ingest.sentry.io',
        'sentry.io',
        'maps.googleapis.com',
    ],
    // report-uri is not supported in the meta tag.
    },
    // We must disable nonce and hash for styles. It's supported by styled-components
    // (see https://github.com/styled-components/styled-components/issues/887) but not easily
    // by material-ui (see https://material-ui.com/styles/advanced/#how-does-one-implement-csp)
    // Since all this is statically generated anyway, it defeats the purpose on nonce anyway
    // (see https://stackoverflow.com/questions/42922784/what-s-the-purpose-of-the-html-nonce-attribute-for-script-and-style-elements).
    // We leave hash for script since they won't budge after generation.
    {
        enabled: true,
        hashEnabled: {
            'script-src': true,
            'style-src': false,
        },
        nonceEnabled: {
            'script-src': false,
            'style-src': false,
        },
    },
)
[1]They can have their own security risk. I'm not export and I don't have a good link to provide right now. It can change in the future. From what I know properly securing cookies is most likely the best thing to do security wise if you can do it.