Skip to content

Write CSP-Compliant Code

Learn how to write applications that comply with Airbase's Content Security Policy (CSP) requirements.

Critical Requirement

Airbase enforces strict CSP headers for all deployed applications. Applications with CSP violations will not function correctly.

What You'll Learn

  • The golden rules of CSP compliance
  • How to avoid inline scripts and event handlers
  • Framework-specific guidance
  • How to test CSP compliance locally

The Golden Rules

Rule 1: No Inline Scripts

❌ Never do this:

<script>
  console.log('Hello World');
  // Any JavaScript code here
</script>

✅ Always do this:

<script src="/js/main.js"></script>

Place all JavaScript code in external .js files and reference them with <script src="..."></script> tags.

Rule 2: No Inline Event Handlers

❌ Never do this:

<button onclick="handleClick()">Click me</button>
<body onload="init()">
<div onmouseover="showTooltip()">

✅ Always do this:

<button id="myButton">Click me</button>
// In external JavaScript file
document.getElementById('myButton').addEventListener('click', handleClick);

Rule 3: No eval() or Similar Functions

❌ Never do this:

eval('console.log("hello")');
new Function('return 2 + 2')();
setTimeout('doSomething()', 1000);

✅ Always do this:

console.log('hello');
const add = () => 2 + 2;
setTimeout(doSomething, 1000);

Rule 4: Use External Files Only

All JavaScript must be in external files served from the same origin (your application).


Framework-Specific Guidance

React / Vite Applications

Good news: Vite bundles all code into external JavaScript files by default!

What to watch out for:

// ❌ Don't use dangerouslySetInnerHTML with scripts
function BadComponent() {
  return (
    <div dangerouslySetInnerHTML={{
      __html: '<script>alert("bad")</script>'
    }} />
  );
}

// ✅ Use normal JSX
function GoodComponent() {
  return (
    <div>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}

Vite CSP compatibility:

  • ✅ Component code → bundled into external JS files
  • onClick handlers → work fine (they're not inline HTML)
  • ✅ CSS-in-JS → Vite handles it correctly
  • ✅ Dynamic imports → work fine

Next.js Applications

Good news: Next.js 13+ is CSP-compliant by default!

What to watch out for:

// ❌ Don't add inline scripts in _document.js
export default function Document() {
  return (
    <Html>
      <Head>
        <script dangerouslySetInnerHTML={{__html: 'console.log("bad")'}} />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

// ✅ Use external scripts or Next.js Script component
import Script from 'next/script';

export default function Document() {
  return (
    <Html>
      <Head>
        <Script src="/scripts/analytics.js" />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

Next.js CSP compatibility:

  • ✅ Server components → CSP-compliant
  • ✅ Client components → CSP-compliant
  • next/script → Use with src attribute
  • ✅ API routes → Not affected by CSP

Static HTML Applications

For plain HTML apps, follow these patterns:

HTML structure:

<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
  <!-- ✅ External CSS is fine -->
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <h1>Hello World</h1>
  <button id="myButton">Click me</button>

  <!-- ✅ External JavaScript at the end -->
  <script src="/js/main.js"></script>
</body>
</html>

JavaScript file (/js/main.js):

// ✅ All JavaScript in external file
document.addEventListener('DOMContentLoaded', function() {
  document.getElementById('myButton').addEventListener('click', function() {
    console.log('Button clicked!');
  });
});

Key points:

  • Load external .js files
  • Use addEventListener for all event handling
  • Use DOMContentLoaded to ensure DOM is ready

Python Applications (Flask, Streamlit)

Most Python frameworks generate CSP-compliant output by default.

Flask example:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    # ✅ Jinja2 templates with external JS are fine
    return render_template('index.html')

Template (templates/index.html):

<!DOCTYPE html>
<html>
<head>
  <title>Flask App</title>
</head>
<body>
  <h1>{{ title }}</h1>
  <!-- ✅ External JavaScript -->
  <script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>

Streamlit:

Streamlit generally works fine with CSP. However, some visualization libraries may generate inline scripts.

If you encounter CSP issues with Python libraries:

  1. Check if the library has CSP configuration options
  2. Update to the latest library version
  3. As a last resort, see Nginx Proxy Workaround

Common Patterns and Solutions

Pattern: Dynamic Content

❌ Wrong way:

element.innerHTML = '<script>doSomething()</script>';

✅ Right way:

// Create elements programmatically
const button = document.createElement('button');
button.textContent = 'Click me';
button.addEventListener('click', doSomething);
element.appendChild(button);

Pattern: Analytics/Tracking

❌ Wrong way:

<script>
  gtag('config', 'GA_MEASUREMENT_ID');
</script>

✅ Right way:

<!-- External script -->
<script src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
<script src="/js/analytics.js"></script>
// /js/analytics.js
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');

Pattern: Configuration/Data Injection

❌ Wrong way:

<script>
  const config = { apiUrl: '{{ api_url }}' };
</script>

✅ Right way:

<!-- Use data attributes -->
<div id="app" data-api-url="{{ api_url }}"></div>
<script src="/js/app.js"></script>
// /js/app.js
const app = document.getElementById('app');
const config = {
  apiUrl: app.dataset.apiUrl
};

Pattern: Form Submission

❌ Wrong way:

<form onsubmit="return handleSubmit()">

✅ Right way:

<form id="myForm">
  <input type="text" name="name">
  <button type="submit">Submit</button>
</form>
<script src="/js/form-handler.js"></script>
// /js/form-handler.js
document.getElementById('myForm').addEventListener('submit', function(e) {
  e.preventDefault();
  // Handle form submission
});

Testing CSP Compliance Locally

Before deploying to Airbase, test your application with CSP headers locally.

For Node.js/Express Apps

Install helmet:

npm install helmet

Add CSP middleware:

const helmet = require('helmet');

app.use(helmet.contentSecurityPolicy({
  directives: {
    scriptSrc: ["'self'"],
    defaultSrc: ["'self'"]
  }
}));

For Static Sites

Use a local server that supports custom headers.

Example with Python:

# server.py
from http.server import HTTPServer, SimpleHTTPRequestHandler

class CSPRequestHandler(SimpleHTTPRequestHandler):
    def end_headers(self):
        self.send_header('Content-Security-Policy', "script-src 'self'")
        SimpleHTTPRequestHandler.end_headers(self)

HTTPServer(('localhost', 8000), CSPRequestHandler).serve_forever()

Run: python server.py

Verification Steps

  1. Start your local server with CSP headers
  2. Open the application in a browser
  3. Open browser console (F12)
  4. Look for CSP violation errors
  5. Fix any violations before deploying

Quick Checklist

Before deploying to Airbase, verify:

  • No <script> tags with inline JavaScript
  • No inline event handlers (onclick, onload, etc.)
  • All JavaScript in external .js files
  • No eval(), Function(), or similar dynamic code
  • No setTimeout/setInterval with string arguments
  • Framework build output doesn't inject inline scripts
  • Tested locally with CSP headers
  • Browser console shows no CSP violations

See Also


Summary

Remember the golden rules:

  1. ✅ External JavaScript files only
  2. ✅ Use addEventListener for events
  3. ✅ No eval() or dynamic code execution
  4. ✅ Test with CSP headers before deploying

Following these guidelines ensures your application will work correctly on Airbase!