diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 000000000..defb790c4 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,16 @@ +name: 'IPTVnator CodeQL config' + +# The mock servers are development/E2E fixtures. They bind to localhost, serve +# fabricated data, ship in no released artifact, and deliberately imitate the +# quirks of the upstream IPTV protocols — including reading a session token +# from a GET query string, which is what the real Stalker backend proxy does +# and therefore what the app must be tested against. +# +# CodeQL's web-service hygiene rules (missing rate limiting, sensitive data in +# GET requests) assume an internet-facing service and produce only false +# positives here; a rate limiter on a fixture that the E2E suite hammers would +# actively break the tests. Injection, path-traversal and similar rules still +# apply to everything the app itself ships. +paths-ignore: + - apps/stalker-mock-server + - apps/xtream-mock-server diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7f87287f3..003b8b879 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,6 +47,9 @@ jobs: uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} + # Excludes the localhost dev/E2E mock servers from analysis; see the + # config file for why. + config-file: ./.github/codeql/codeql-config.yml # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. diff --git a/apps/stalker-mock-server/src/app/auth-store.ts b/apps/stalker-mock-server/src/app/auth-store.ts index 8d2f7d4bf..28680de9e 100644 --- a/apps/stalker-mock-server/src/app/auth-store.ts +++ b/apps/stalker-mock-server/src/app/auth-store.ts @@ -95,7 +95,9 @@ export function readBearerToken(req: Request): string | undefined { if (typeof header !== 'string') { return undefined; } - const match = /Bearer\s+(.*)$/i.exec(header.trim()); + // `\s+(.*)` would backtrack polynomially on "bearer" + a long run of + // spaces, so require the token to start with a non-space character. + const match = /^Bearer[ \t]+(\S.*)$/i.exec(header.trim()); return match?.[1]?.trim() || undefined; } diff --git a/apps/stalker-mock-server/src/main.ts b/apps/stalker-mock-server/src/main.ts index 2f76cd737..a98a2bdde 100644 --- a/apps/stalker-mock-server/src/main.ts +++ b/apps/stalker-mock-server/src/main.ts @@ -100,14 +100,21 @@ app.get('/stalker', (req: Request, res: Response) => { url: portalUrl, token, ...rest - } = req.query as Record; - const mac = macAddress ?? '00:1a:79:00:00:01'; + } = req.query as Record; + + // A repeated query key arrives as an array, so every value used below must + // be narrowed to a string before it reaches a string API. + const asString = (value: unknown): string | undefined => + typeof value === 'string' ? value : undefined; + + const mac = asString(macAddress) ?? '00:1a:79:00:00:01'; // The real backend proxy turns the token query param into a Bearer header // before calling the portal; mirror that so token handling is exercised. const headers: Record = { cookie: `mac=${mac}` }; - if (token) { - headers['authorization'] = `Bearer ${token}`; + const bearer = asString(token); + if (bearer) { + headers['authorization'] = `Bearer ${bearer}`; } // Build a lightweight synthetic request. We need a fresh object with mutable @@ -135,7 +142,7 @@ app.get('/stalker', (req: Request, res: Response) => { dispatchPortalAction(syntheticReq, syntheticRes, { // The proxied portal URL decides strictness, matching the two direct // endpoints: a canonical Ministra path enforces the token. - enforceAuth: (portalUrl ?? '').includes('/stalker_portal/'), + enforceAuth: (asString(portalUrl) ?? '').includes('/stalker_portal/'), }); // The portal answers auth failures with a plain-text body; the real backend @@ -163,7 +170,8 @@ app.post('/reset', (_req: Request, res: Response) => { * retries instead of surfacing an error. */ app.post('/invalidate-session', (req: Request, res: Response) => { - const mac = String(req.query['macAddress'] ?? ''); + const macParam = req.query['macAddress']; + const mac = typeof macParam === 'string' ? macParam : ''; if (!mac) { res.status(400).json({ error: 'macAddress query param is required' }); return;