mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
fix(wiki): tighten CSP with nonce and stricter LLM error matching
Address PR review feedback: replace 'unsafe-inline' script-src with a per-build nonce, and parse error JSON before matching max_tokens rejection to avoid false positives.
This commit is contained in:
parent
505228b851
commit
c2e9190405
3 changed files with 43 additions and 19 deletions
|
|
@ -5,6 +5,7 @@
|
|||
* module tree, and metadata — viewable offline in any browser.
|
||||
*/
|
||||
|
||||
import { randomBytes } from 'crypto';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
|
|
@ -90,6 +91,8 @@ function buildHTML(
|
|||
const treeJSON = safeJSON(moduleTree);
|
||||
const metaJSON = safeJSON(meta);
|
||||
|
||||
const nonce = randomBytes(16).toString('base64');
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
// ── Head ──
|
||||
|
|
@ -98,11 +101,11 @@ function buildHTML(
|
|||
parts.push('<head>');
|
||||
parts.push('<meta charset="UTF-8">');
|
||||
parts.push('<meta name="viewport" content="width=device-width, initial-scale=1.0">');
|
||||
parts.push('<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; script-src \'unsafe-inline\' \'unsafe-eval\' https://cdn.jsdelivr.net; style-src \'unsafe-inline\'; img-src data: https:;">');
|
||||
parts.push(`<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}' 'unsafe-eval' https://cdn.jsdelivr.net; style-src 'unsafe-inline'; img-src data: https:;">`);
|
||||
parts.push('<title>' + esc(projectName) + ' — Wiki</title>');
|
||||
parts.push('<script src="https://cdn.jsdelivr.net/npm/marked@11.0.0/marked.min.js"><\/script>');
|
||||
parts.push(`<script nonce="${nonce}" src="https://cdn.jsdelivr.net/npm/marked@11.0.0/marked.min.js"><\/script>`);
|
||||
parts.push(
|
||||
'<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"><\/script>',
|
||||
`<script nonce="${nonce}" src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"><\/script>`,
|
||||
);
|
||||
parts.push('<style>');
|
||||
parts.push(CSS);
|
||||
|
|
@ -136,7 +139,7 @@ function buildHTML(
|
|||
parts.push('</div>');
|
||||
|
||||
// ── Script ──
|
||||
parts.push('<script>');
|
||||
parts.push(`<script nonce="${nonce}">`);
|
||||
parts.push('var PAGES = ' + pagesJSON + ';');
|
||||
parts.push('var TREE = ' + treeJSON + ';');
|
||||
parts.push('var META = ' + metaJSON + ';');
|
||||
|
|
|
|||
|
|
@ -211,11 +211,16 @@ export async function callLLM(
|
|||
continue;
|
||||
}
|
||||
|
||||
// Auto-switch max_tokens → max_completion_tokens when the model rejects max_tokens
|
||||
// Auto-switch max_tokens -> max_completion_tokens when the model rejects it
|
||||
let tokenErrMsg = errorText;
|
||||
try {
|
||||
const parsed = JSON.parse(errorText);
|
||||
if (parsed?.error?.message) tokenErrMsg = parsed.error.message;
|
||||
} catch { /* use raw errorText */ }
|
||||
if (
|
||||
!switchedTokenParam &&
|
||||
response.status === 400 &&
|
||||
(errorText.includes("'max_tokens'") || errorText.includes('"max_tokens"')) &&
|
||||
/['"]max_tokens['"]/.test(tokenErrMsg) &&
|
||||
body.max_tokens !== undefined
|
||||
) {
|
||||
console.warn(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import fs from 'fs/promises';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
|
@ -54,17 +54,33 @@ describe('safeJSON', () => {
|
|||
});
|
||||
|
||||
describe('generateHTMLViewer — CSP meta tag', () => {
|
||||
it('includes a Content-Security-Policy meta tag in generated HTML', async () => {
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'wiki-csp-'));
|
||||
try {
|
||||
await fs.writeFile(path.join(tmpDir, 'overview.md'), '# Hello');
|
||||
await fs.writeFile(path.join(tmpDir, 'module_tree.json'), '[]');
|
||||
const outputPath = await generateHTMLViewer(tmpDir, 'TestProject');
|
||||
const html = await fs.readFile(outputPath, 'utf-8');
|
||||
expect(html).toContain('Content-Security-Policy');
|
||||
expect(html).toContain("'unsafe-eval'");
|
||||
} finally {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
let tmpDir: string;
|
||||
let html: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'wiki-csp-'));
|
||||
await fs.writeFile(path.join(tmpDir, 'overview.md'), '# Hello');
|
||||
await fs.writeFile(path.join(tmpDir, 'module_tree.json'), '[]');
|
||||
const outputPath = await generateHTMLViewer(tmpDir, 'TestProject');
|
||||
html = await fs.readFile(outputPath, 'utf-8');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('includes a nonce-based Content-Security-Policy (no unsafe-inline for scripts)', () => {
|
||||
expect(html).toContain('Content-Security-Policy');
|
||||
expect(html).toContain("'unsafe-eval'");
|
||||
expect(html).toMatch(/script-src 'nonce-[A-Za-z0-9+/=]+'/);;
|
||||
expect(html).not.toMatch(/script-src[^;]*'unsafe-inline'/);
|
||||
});
|
||||
|
||||
it('applies the nonce attribute to every <script> tag', () => {
|
||||
const scriptTags = html.match(/<script[\s>]/g) ?? [];
|
||||
expect(scriptTags.length).toBeGreaterThanOrEqual(3);
|
||||
|
||||
const noncedScripts = html.match(/<script\s+nonce="[A-Za-z0-9+/=]+"[\s>]/g) ?? [];
|
||||
expect(noncedScripts.length).toBe(scriptTags.length);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue