remove username as public input from /sponsors/github

Extends the previous commit's fix. The only caller (Donate.vue via
apiService.getSponsors()) never sends a username and always relied on the
server-side default, so the parameter had no legitimate use even after
being parameterized — it just let anyone use deflock's server as an
unauthenticated relay onto GitHub's GraphQL API under the server's own
token. Drop it entirely: the route no longer accepts a username
querystring param, and GithubClient.getSponsors() takes no argument,
hardcoding the maintainer's login via MAINTAINER_USERNAME. Keeps the
$username GraphQL-variable parameterization technique so reintroducing a
variable input later can't reintroduce the injection.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Chris Bowles
2026-07-08 02:33:12 -04:00
parent 3eceedb9c6
commit 34e2cbdc2d
4 changed files with 61 additions and 43 deletions
+49 -25
View File
@@ -1,36 +1,60 @@
import { describe, it, expect } from 'bun:test';
import { buildSponsorsQuery } from './GithubClient';
import { describe, it, expect, afterEach, mock } from 'bun:test';
import { GithubClient, buildSponsorsQuery } from './GithubClient';
describe('buildSponsorsQuery', () => {
it('parameterizes the username instead of interpolating it', () => {
const { query, variables } = buildSponsorsQuery('frillweeman');
expect(query).toContain('query($login: String!)');
expect(query).toContain('user(login: $login)');
expect(variables).toEqual({ login: 'frillweeman' });
});
it('never places the raw username inside the query string', () => {
const { query } = buildSponsorsQuery('octocat');
// The query is fixed and only references the $login variable — the caller's
// value must not appear in the query text itself.
expect(query).not.toContain('octocat');
});
it('keeps GraphQL-breaking input confined to the variables (injection guard)', () => {
// A username containing a quote used to break out of the interpolated
// string literal. It must now travel only in `variables.login`.
const hostile = '") { viewer { login } } #';
const { query, variables } = buildSponsorsQuery(hostile);
expect(variables.login).toBe(hostile);
expect(query).not.toContain(hostile);
expect(query).not.toContain('viewer');
it('parameterizes the username via a $username GraphQL variable instead of interpolating it', () => {
const { query, variables } = buildSponsorsQuery();
expect(query).toContain('query($username: String!)');
expect(query).toContain('user(login: $username)');
expect(variables).toEqual({ username: 'frillweeman' });
});
it('requests sponsorships and the expected sponsor fields', () => {
const { query } = buildSponsorsQuery('frillweeman');
const { query } = buildSponsorsQuery();
expect(query).toContain('sponsorshipsAsMaintainer(first: 100)');
for (const field of ['login', 'name', 'avatarUrl', 'url']) {
expect(query).toContain(field);
}
});
});
describe('GithubClient.getSponsors', () => {
const originalFetch = global.fetch;
afterEach(() => {
global.fetch = originalFetch;
});
it('sends exactly the query and variables built by buildSponsorsQuery', async () => {
let capturedBody: any;
global.fetch = mock(async (_url: string, init: RequestInit) => {
capturedBody = JSON.parse(init.body as string);
return new Response(JSON.stringify({ data: { user: { sponsorshipsAsMaintainer: { nodes: [] } } } }));
}) as unknown as typeof fetch;
const client = new GithubClient();
await client.getSponsors();
expect(capturedBody).toEqual(buildSponsorsQuery());
});
it('returns the sponsor nodes from the response', async () => {
const nodes = [{ sponsor: { login: 'someone', name: 'Someone', avatarUrl: 'https://example.com/a.png', url: 'https://github.com/someone' } }];
global.fetch = mock(async () =>
new Response(JSON.stringify({ data: { user: { sponsorshipsAsMaintainer: { nodes } } } }))
) as unknown as typeof fetch;
const client = new GithubClient();
const result = await client.getSponsors();
expect(result).toEqual(nodes);
});
it('throws when the GitHub API responds with a non-OK status', async () => {
global.fetch = mock(async () => new Response('', { status: 500 })) as unknown as typeof fetch;
const client = new GithubClient();
await expect(client.getSponsors()).rejects.toThrow('Failed to get sponsors: 500');
});
});