fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

View File

@@ -0,0 +1,143 @@
/**
* @fileoverview CSP checks as used by Lighthouse. These checks tend to be a
* stricter subset of the other checks defined in this project.
*/
import {CheckerFunction} from '../checks/checker';
import {checkInvalidKeyword, checkMissingSemicolon, checkUnknownDirective} from '../checks/parser_checks';
import {checkDeprecatedDirective, checkMissingObjectSrcDirective, checkMissingScriptSrcDirective, checkMultipleMissingBaseUriDirective, checkNonceLength, checkPlainUrlSchemes, checkScriptUnsafeInline, checkWildcards} from '../checks/security_checks';
import {checkAllowlistFallback, checkStrictDynamic, checkUnsafeInlineFallback} from '../checks/strictcsp_checks';
import {Csp, Directive, Version} from '../csp';
import {Finding} from '../finding';
interface Equalable {
equals(a: unknown): boolean;
}
function arrayContains<T extends Equalable>(arr: T[], elem: T) {
return arr.some(e => e.equals(elem));
}
/**
* Computes the intersection of all of the given sets using the `equals(...)`
* method to compare items.
*/
function setIntersection<T extends Equalable>(sets: T[][]): T[] {
const intersection: T[] = [];
if (sets.length === 0) {
return intersection;
}
const firstSet = sets[0];
for (const elem of firstSet) {
if (sets.every(set => arrayContains(set, elem))) {
intersection.push(elem);
}
}
return intersection;
}
/**
* Computes the union of all of the given sets using the `equals(...)` method to
* compare items.
*/
function setUnion<T extends Equalable>(sets: T[][]): T[] {
const union: T[] = [];
for (const set of sets) {
for (const elem of set) {
if (!arrayContains(union, elem)) {
union.push(elem);
}
}
}
return union;
}
/**
* Checks if *any* of the given policies pass the given checker. If at least one
* passes, returns no findings. Otherwise, returns the list of findings from the
* first one that had any findings.
*/
function atLeastOnePasses(
parsedCsps: Csp[], checker: CheckerFunction): Finding[] {
const findings: Finding[][] = [];
for (const parsedCsp of parsedCsps) {
findings.push(checker(parsedCsp));
}
return setIntersection(findings);
}
/**
* Checks if *any* of the given policies fail the given checker. Returns the
* list of findings from the one that had the most findings.
*/
function atLeastOneFails(
parsedCsps: Csp[], checker: CheckerFunction): Finding[] {
const findings: Finding[][] = [];
for (const parsedCsp of parsedCsps) {
findings.push(checker(parsedCsp));
}
return setUnion(findings);
}
/**
* Evaluate the given list of CSPs for checks that should cause Lighthouse to
* mark the CSP as failing. Returns only the first set of failures.
*/
export function evaluateForFailure(parsedCsps: Csp[]): Finding[] {
// Check #1
const targetsXssFindings = [
...atLeastOnePasses(parsedCsps, checkMissingScriptSrcDirective),
...atLeastOnePasses(parsedCsps, checkMissingObjectSrcDirective),
...checkMultipleMissingBaseUriDirective(parsedCsps),
];
// Check #2
const effectiveCsps =
parsedCsps.map(csp => csp.getEffectiveCsp(Version.CSP3));
const effectiveCspsWithScript = effectiveCsps.filter(csp => {
const directiveName = csp.getEffectiveDirective(Directive.SCRIPT_SRC);
return csp.directives[directiveName];
});
const robust = [
...atLeastOnePasses(effectiveCspsWithScript, checkStrictDynamic),
...atLeastOnePasses(effectiveCspsWithScript, checkScriptUnsafeInline),
...atLeastOnePasses(effectiveCsps, checkWildcards),
...atLeastOnePasses(effectiveCsps, checkPlainUrlSchemes),
];
return [...targetsXssFindings, ...robust];
}
/**
* Evaluate the given list of CSPs for checks that should cause Lighthouse to
* mark the CSP as OK, but present a warning. Returns only the first set of
* failures.
*/
export function evaluateForWarnings(parsedCsps: Csp[]): Finding[] {
// Check #1 is implemented by Lighthouse directly
// Check #2 is no longer used in Lighthouse.
// Check #3
return [
...atLeastOneFails(parsedCsps, checkUnsafeInlineFallback),
...atLeastOneFails(parsedCsps, checkAllowlistFallback)
];
}
/**
* Evaluate the given list of CSPs for syntax errors. Returns a list of the same
* length as parsedCsps where each item in the list is the findings for the
* matching Csp.
*/
export function evaluateForSyntaxErrors(parsedCsps: Csp[]): Finding[][] {
// Check #4
const allFindings: Finding[][] = [];
for (const csp of parsedCsps) {
const findings = [
...checkNonceLength(csp), ...checkUnknownDirective(csp),
...checkDeprecatedDirective(csp), ...checkMissingSemicolon(csp),
...checkInvalidKeyword(csp)
];
allFindings.push(findings);
}
return allFindings;
}

View File

@@ -0,0 +1,538 @@
/**
* @fileoverview Tests for CSP Parser checks.
*/
import 'jasmine';
import {Csp,} from '../csp';
import {Severity} from '../finding';
import {CspParser} from '../parser';
import * as lighthouseChecks from './lighthouse_checks';
function parsePolicies(policies: string[]): Csp[] {
return policies.map(p => (new CspParser(p)).csp);
}
describe('Test evaluateForFailure', () => {
it('robust nonce-based policy', () => {
const test =
'script-src \'nonce-aaaaaaaaaa\'; object-src \'none\'; base-uri \'none\'';
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies([test]));
expect(violations.length).toBe(0);
});
it('robust hash-based policy', () => {
const test = 'script-src \'sha256-aaaaaaaaaa\'; object-src \'none\'';
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies([test]));
expect(violations.length).toBe(0);
});
it('policy not attempt', () => {
const test = 'block-all-mixed-content';
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies([test]));
expect(violations.length).toBe(2);
expect(violations[0].severity).toBe(Severity.HIGH);
expect(violations[0].directive).toBe('script-src');
expect(violations[0].description).toBe('script-src directive is missing.');
expect(violations[1].severity).toBe(Severity.HIGH);
expect(violations[1].directive).toBe('object-src');
expect(violations[1].description)
.toBe(
`Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`);
});
it('policy not robust', () => {
const test = 'script-src *.google.com; object-src \'none\'';
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies([test]));
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.STRICT_CSP);
expect(violations[0].directive).toBe('script-src');
expect(violations[0].description)
.toBe(
`Host allowlists can frequently be bypassed. Consider using 'strict-dynamic' in combination with CSP nonces or hashes.`);
});
it('robust policy and not robust policy', () => {
const policies = [
'script-src *.google.com; object-src \'none\'',
'script-src \'nonce-aaaaaaaaaa\'; base-uri \'none\''
];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('split across many policies', () => {
const policies = [
'object-src \'none\'', 'script-src \'nonce-aaaaaaaaaa\'',
'base-uri \'none\''
];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('split across many policies with default-src', () => {
const policies = ['default-src \'none\'', 'base-uri \'none\''];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('split across many policies some mixed useless policies', () => {
const policies = [
'object-src \'none\'', 'script-src \'nonce-aaaaaaaaaa\'',
'base-uri \'none\'', 'block-all-mixed-content'
];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('split across many policies with allowlist', () => {
const policies = [
'object-src \'none\'', 'script-src \'nonce-aaaaaaaaaa\'',
'base-uri \'none\'', 'script-src *'
];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('not robust and not attempt', () => {
const policies = ['block-all-mixed-content', 'script-src *.google.com'];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(2);
expect(violations[0].severity).toBe(Severity.HIGH);
expect(violations[0].directive).toBe('object-src');
expect(violations[0].description)
.toBe(
`Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`);
expect(violations[1].severity).toBe(Severity.STRICT_CSP);
expect(violations[1].directive).toBe('script-src');
expect(violations[1].description)
.toBe(
`Host allowlists can frequently be bypassed. Consider using \'strict-dynamic\' in combination with CSP nonces or hashes.`);
});
it('robust check only CSPs with script-src', () => {
const policies = ['script-src https://example.com', 'object-src \'none\''];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.STRICT_CSP);
expect(violations[0].directive).toBe('script-src');
expect(violations[0].description)
.toBe(
`Host allowlists can frequently be bypassed. Consider using \'strict-dynamic\' in combination with CSP nonces or hashes.`);
});
it('two not attempt', () => {
const policies = ['block-all-mixed-content', 'block-all-mixed-content'];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(2);
expect(violations[0].severity).toBe(Severity.HIGH);
expect(violations[0].directive).toBe('script-src');
expect(violations[0].description).toBe('script-src directive is missing.');
expect(violations[1].severity).toBe(Severity.HIGH);
expect(violations[1].directive).toBe('object-src');
expect(violations[1].description)
.toBe(
`Missing object-src allows the injection of plugins which can execute JavaScript. Can you set it to 'none'?`);
});
it('two not attempt somewhat', () => {
const policies = [
'block-all-mixed-content; object-src \'none\'',
'block-all-mixed-content',
];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.HIGH);
expect(violations[0].directive).toBe('script-src');
expect(violations[0].description).toBe('script-src directive is missing.');
});
it('base-uri split across many policies', () => {
const policies = [
'script-src \'nonce-aaaaaaaaaaa\'; object-src \'none\'',
'base-uri \'none\'',
];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('base-uri not set', () => {
const policies = [
'script-src \'nonce-aaaaaaaaaaa\'; object-src \'none\'',
];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.HIGH);
expect(violations[0].directive).toBe('base-uri');
expect(violations[0].description)
.toBe(
`Missing base-uri allows the injection of base tags. They can be used to set the base URL for all relative (script) URLs to an attacker controlled domain. Can you set it to 'none' or 'self'?`);
});
it('base-uri not set in either policy', () => {
const policies = [
'script-src \'nonce-aaaaaaaaaaa\'; object-src \'none\'',
'block-all-mixed-content'
];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.HIGH);
expect(violations[0].directive).toBe('base-uri');
});
it('check wildcards', () => {
const policies = ['script-src \'none\'; object-src *'];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.HIGH);
expect(violations[0].directive).toBe('object-src');
expect(violations[0].description)
.toBe(`object-src should not allow '*' as source`);
});
it('check wildcards on multiple', () => {
const policies =
['script-src \'none\'; object-src *', 'object-src \'none\''];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('check plain url schemes', () => {
const policies = [
`script-src 'strict-dynamic' 'nonce-random123' 'unsafe-inline' https:; base-uri 'none'; object-src https:`
];
const violations =
lighthouseChecks.evaluateForFailure(parsePolicies(policies));
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.HIGH);
expect(violations[0].directive).toBe('object-src');
expect(violations[0].description)
.toBe(
`https: URI in object-src allows the execution of unsafe scripts.`);
});
});
describe('Test evaluateForWarnings', () => {
it('perfect', () => {
const test =
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url';
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies([test]));
expect(violations.length).toBe(0);
});
it('perfect except some failures', () => {
const policies = [
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; object-src \'none\'',
'block-all-mixed-content'
];
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('a perfect policy and a policy that does not target', () => {
const policies = [
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; object-src \'none\'',
'block-all-mixed-content'
];
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('perfect policy split into two', () => {
const policies = [
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; ',
'block-all-mixed-content; object-src \'none\''
];
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('perfect policy split into three', () => {
const policies = [
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; ',
'block-all-mixed-content', 'object-src \'none\''
];
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('no reporting and malformed', () => {
const test = 'script-src \'nonce-aaaaaaaaaa\'; unknown-directive';
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies([test]));
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.STRICT_CSP);
expect(violations[0].directive).toBe('script-src');
expect(violations[0].description)
.toBe(
'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.');
});
it('missing unsafe-inline fallback', () => {
const test = 'script-src \'nonce-aaaaaaaaaa\'; report-uri url';
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies([test]));
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.STRICT_CSP);
expect(violations[0].directive).toBe('script-src');
expect(violations[0].description)
.toBe(
'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.');
});
it('missing allowlist fallback', () => {
const test =
'script-src \'nonce-aaaaaaaaaa\' \'strict-dynamic\' \'unsafe-inline\'; report-uri url';
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies([test]));
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.STRICT_CSP);
expect(violations[0].directive).toBe('script-src');
expect(violations[0].description)
.toBe(
'Consider adding https: and http: url schemes (ignored by browsers supporting \'strict-dynamic\') to be backward compatible with older browsers.');
});
it('missing semicolon', () => {
const test =
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url object-src \'self\'';
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies([test]));
expect(violations.length).toBe(0);
});
it('invalid keyword', () => {
const test =
'script-src \'nonce-aaaaaaaaa\' \'invalid\' \'unsafe-inline\'; report-uri url';
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies([test]));
expect(violations.length).toBe(0);
});
it('perfect policy and invalid policy', () => {
const policies = [
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:; report-uri url; base-uri \'none\'; object-src \'none\'',
'unknown'
];
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('reporting on the wrong policy', () => {
const policies = [
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:',
'block-all-mixed-content; report-uri url'
];
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies(policies));
expect(violations.length).toBe(0);
});
it('missing unsafe-inline fallback split over two policies', () => {
const policies = [
'script-src \'nonce-aaaaaaaaaa\'',
'block-all-mixed-content; report-uri url'
];
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies(policies));
expect(violations.length).toBe(1);
expect(violations[0].severity).toBe(Severity.STRICT_CSP);
expect(violations[0].directive).toBe('script-src');
expect(violations[0].description)
.toBe(
'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.');
});
it('strict-dynamic with no fallback in any policy', () => {
const policies = [
'script-src \'nonce-aaaaaaaaaa\' \'strict-dynamic\'',
'block-all-mixed-content; report-uri url'
];
const violations =
lighthouseChecks.evaluateForWarnings(parsePolicies(policies));
expect(violations.length).toBe(2);
expect(violations[0].severity).toBe(Severity.STRICT_CSP);
expect(violations[0].directive).toBe('script-src');
expect(violations[0].description)
.toBe(
'Consider adding \'unsafe-inline\' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.');
expect(violations[1].severity).toBe(Severity.STRICT_CSP);
expect(violations[1].directive).toBe('script-src');
expect(violations[1].description)
.toBe(
'Consider adding https: and http: url schemes (ignored by browsers supporting \'strict-dynamic\') to be backward compatible with older browsers.');
});
});
describe('Test evaluateForSyntaxErrors', () => {
it('whenPerfectPolicies', () => {
const policies = [
'script-src \'nonce-aaaaaaaaaa\' \'unsafe-inline\' http: https:',
'block-all-mixed-content; report-uri url'
];
const violations =
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies(policies));
expect(violations.length).toBe(2);
expect(violations[0].length).toBe(0);
expect(violations[1].length).toBe(0);
});
it('whenShortNonce', () => {
const test = 'script-src \'nonce-a\' \'unsafe-inline\'; report-uri url';
const violations =
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test]));
expect(violations.length).toBe(1);
expect(violations[0].length).toBe(1);
expect(violations[0][0].severity).toBe(Severity.MEDIUM);
expect(violations[0][0].directive).toBe('script-src');
expect(violations[0][0].description)
.toBe('Nonces should be at least 8 characters long.');
});
it('whenUnknownDirective', () => {
const test =
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url; unknown';
const violations =
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test]));
expect(violations.length).toBe(1);
expect(violations[0].length).toBe(1);
expect(violations[0][0].severity).toBe(Severity.SYNTAX);
expect(violations[0][0].directive).toBe('unknown');
expect(violations[0][0].description)
.toBe('Directive "unknown" is not a known CSP directive.');
});
it('whenDeprecatedDirective', () => {
const test =
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url; reflected-xss foo';
const violations =
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test]));
expect(violations.length).toBe(1);
expect(violations[0].length).toBe(1);
expect(violations[0][0].severity).toBe(Severity.INFO);
expect(violations[0][0].directive).toBe('reflected-xss');
expect(violations[0][0].description)
.toBe(
'reflected-xss is deprecated since CSP2. Please, use the X-XSS-Protection header instead.');
});
it('whenMissingSemicolon', () => {
const test =
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; report-uri url object-src \'none\'';
const violations =
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test]));
expect(violations.length).toBe(1);
expect(violations[0].length).toBe(1);
expect(violations[0][0].severity).toBe(Severity.SYNTAX);
expect(violations[0][0].directive).toBe('report-uri');
expect(violations[0][0].description)
.toBe(
'Did you forget the semicolon? "object-src" seems to be a directive, not a value.');
});
it('whenInvalidKeyword', () => {
const test =
'script-src \'nonce-aaaaaaaaa\' \'unsafe-inline\'; object-src \'invalid\'';
const violations =
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies([test]));
expect(violations.length).toBe(1);
expect(violations[0].length).toBe(1);
expect(violations[0][0].severity).toBe(Severity.SYNTAX);
expect(violations[0][0].directive).toBe('object-src');
expect(violations[0][0].description)
.toBe('\'invalid\' seems to be an invalid CSP keyword.');
});
it('manyPolicies', () => {
const policies = [
'object-src \'invalid\'', 'script-src \'none\'',
'script-src \'nonce-short\' default-src \'none\''
];
const violations =
lighthouseChecks.evaluateForSyntaxErrors(parsePolicies(policies));
expect(violations.length).toBe(3);
expect(violations[0].length).toBe(1);
expect(violations[0][0].severity).toBe(Severity.SYNTAX);
expect(violations[0][0].directive).toBe('object-src');
expect(violations[0][0].description)
.toBe('\'invalid\' seems to be an invalid CSP keyword.');
expect(violations[1].length).toBe(0);
expect(violations[2].length).toBe(2);
expect(violations[2][0].severity).toBe(Severity.MEDIUM);
expect(violations[2][0].directive).toBe('script-src');
expect(violations[2][0].description)
.toBe('Nonces should be at least 8 characters long.');
expect(violations[2][1].severity).toBe(Severity.SYNTAX);
expect(violations[2][1].directive).toBe('script-src');
expect(violations[2][1].description)
.toBe(
'Did you forget the semicolon? "default-src" seems to be a directive, not a value.');
});
});