GitHub buttons look harmless. They’re tiny. They show a star count. They make product people happy.

They also pull in third-party resources, which means they touch your Content Security Policy. If your CSP is strict — and it should be — GitHub buttons are one of those “why is this blocked?” annoyances that show up late in a launch checklist.

Here’s how I handle them without turning CSP into mush.

What GitHub buttons usually load

The most common GitHub button embed looks like this:

<a
  class="github-button"
  href="https://github.com/ntkme/github-buttons"
  data-icon="octicon-star"
  data-size="large"
  data-show-count="true"
  aria-label="Star ntkme/github-buttons on GitHub">
  Star
</a>
<script async defer src="https://buttons.github.io/buttons.js"></script>

That script comes from:

  • https://buttons.github.io

And the button itself links to:

  • https://github.com

Depending on how the script renders and what assets it fetches, you may also need to account for frame or image sources. The exact behavior can change over time, which is why I strongly prefer testing the final page in a browser with CSP reporting or your devtools console open.

If you want a quick way to inspect your current headers and sanity-check what you’re shipping, HeaderTest is useful for spotting obvious CSP mistakes.

The minimum CSP for GitHub buttons

If your page only needs the GitHub buttons script and normal same-origin assets, start here:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://buttons.github.io;
  connect-src 'self';
  img-src 'self' https: data:;
  style-src 'self';
  frame-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

That’s intentionally conservative.

Why these directives

  • script-src 'self' https://buttons.github.io; Allows the GitHub buttons loader.
  • img-src 'self' https: data:; Safe fallback if the widget pulls remote images or inline data URLs.
  • frame-src 'self'; Start tight. If the widget uses an iframe and gets blocked, you’ll see it fast.
  • object-src 'none'; Always do this unless you have a weird legacy requirement.
  • frame-ancestors 'none'; Prevents your page being embedded elsewhere.
  • base-uri 'self'; Stops <base> tag abuse.
  • form-action 'self'; Keeps forms local unless you explicitly allow more.

If you’re new to some of these directives, CSP Guide has solid directive-by-directive explanations without too much fluff.

A realistic policy on a production site

Most real sites already have analytics, consent tooling, and a nonce-based script policy. Here’s a real-world CSP header from headertest.com:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-NzEzNmNmZmUtNTExZS00ZjlmLWE0YTMtMTRkZDY1YjNjMGQx' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

If you wanted to add GitHub buttons to a site with a policy shaped like that, I’d update it like this:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://buttons.github.io;
  style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
  frame-src 'self' https://consentcdn.cookiebot.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';

The only change there is adding https://buttons.github.io to script-src.

That might be enough. It might not. Third-party widgets have a bad habit of evolving quietly.

If the button breaks, check these directives next

When GitHub buttons fail under CSP, it’s usually one of these:

1. script-src

You forgot to allow buttons.github.io.

Browser error will look something like:

Refused to load the script 'https://buttons.github.io/buttons.js'
because it violates the following Content Security Policy directive:
"script-src 'self' ..."

Fix:

script-src 'self' https://buttons.github.io;

2. frame-src

Some button implementations render inside an iframe. If you see a CSP error mentioning a frame, allow the origin actually being used.

Start by testing with this if needed:

frame-src 'self' https://buttons.github.io https://github.com;

I would not add both blindly on day one, but I would absolutely add whichever one the browser tells me is blocked.

3. style-src

Some third-party widgets inject inline styles. If your CSP disallows inline styles and the widget depends on them, you have three choices:

  • accept breakage
  • loosen style-src
  • avoid the widget and build a plain link instead

I lean hard toward the third option if a badge needs unsafe-inline. Tiny social widgets are not worth weakening your policy.

A better option: skip the script entirely

If all you want is a “Star us on GitHub” call to action, don’t use the script. Just use a normal link or your own button styling.

<a class="btn btn-github" href="https://github.com/your-org/your-repo">
  Star us on GitHub
</a>

Then your CSP stays cleaner:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data:;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

This is my default recommendation.

The live star count looks nice, but it’s rarely worth adding another third-party script. Every external script is a trust decision. Treat it that way.

Express example

Here’s how to send a CSP header for GitHub buttons in an Express app:

import express from "express";

const app = express();

app.use((req, res, next) => {
  res.setHeader(
    "Content-Security-Policy",
    [
      "default-src 'self'",
      "script-src 'self' https://buttons.github.io",
      "style-src 'self'",
      "img-src 'self' https: data:",
      "connect-src 'self'",
      "frame-src 'self' https://buttons.github.io https://github.com",
      "object-src 'none'",
      "base-uri 'self'",
      "form-action 'self'",
      "frame-ancestors 'none'",
    ].join("; ")
  );
  next();
});

app.get("/", (req, res) => {
  res.send(`
    <!doctype html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>GitHub Buttons CSP Demo</title>
      </head>
      <body>
        <a
          class="github-button"
          href="https://github.com/ntkme/github-buttons"
          data-icon="octicon-star"
          data-show-count="true"
          aria-label="Star ntkme/github-buttons on GitHub">
          Star
        </a>
        <script async defer src="https://buttons.github.io/buttons.js"></script>
      </body>
    </html>
  `);
});

app.listen(3000, () => {
  console.log("Listening on http://localhost:3000");
});

Nginx example

Same idea in Nginx:

add_header Content-Security-Policy "
  default-src 'self';
  script-src 'self' https://buttons.github.io;
  style-src 'self';
  img-src 'self' https: data:;
  connect-src 'self';
  frame-src 'self' https://buttons.github.io https://github.com;
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
" always;

Use Report-Only first

For third-party widgets, I nearly always roll out in report-only mode first.

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' https://buttons.github.io;
  style-src 'self';
  img-src 'self' https: data:;
  frame-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

That lets you see what would break before users see it.

If you already collect CSP violation reports, even better. If not, at least watch the browser console while loading the page with the widget.

My practical rule for GitHub buttons

Here’s the blunt version:

  • If you need the official widget, allow https://buttons.github.io in script-src.
  • If frames get blocked, allow the exact frame origin shown in the console.
  • Don’t preemptively allow broad GitHub domains “just in case”.
  • If the widget wants inline styles or a bunch of extra origins, replace it with a plain link.

That last one saves a lot of security debt.

A star button should not be the reason your CSP turns into https: 'unsafe-inline' 'unsafe-eval'. I’ve seen teams do that for analytics snippets, chat widgets, badge scripts, and random embeds. It always starts small. Then six months later nobody knows what can actually execute on the page.

Keep the policy tight, add only what the browser proves you need, and be willing to say no to decorative third-party JavaScript.