Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/variables-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ function escapeHtml(s) {
// (see DistService.saveTemplateVariablesFile). Backslash/quote-escape for the JS-string
// context first, then HTML-escape the result so it can't break out of the attribute either.
function escapeJsAttr(s) {
return escapeHtml(String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'"));
return escapeHtml(String(s).replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'"));
}

function showBanner(type, html) {
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/lib/variables-editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,5 +341,26 @@ describe('registerVariablesEditorRoutes', () => {
expect(res.body).not.toContain('id="tab-schema"');
expect(res.body).not.toContain('id="save-btn"');
});

// Every `<script>` block is built via string concatenation inside one giant template
// literal — a backslash-escaping mistake there (e.g. a regex like /\\/) is invisible to
// `tsc`/eslint (it's just characters inside a string) and only breaks at runtime when the
// browser parses it, which none of the tests above would ever catch. `new Function` parses
// without executing, so this fails fast on any embedded-script SyntaxError.
it('embeds syntactically valid client-side JavaScript in every <script> block', async () => {
const app = register({ miAPI: { isTemplateLess: false } as unknown as MiAPI });
const res = makeRes();

app.handlers.get(`GET ${VARIABLES_EDITOR_PATH}`)!({}, res);

const html = res.body as string;
const scripts = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].map((m) => m[1]);

expect(scripts.length).toBeGreaterThan(0);

for (const script of scripts) {
expect(() => new Function(script)).not.toThrow();
}
});
});
});