Skip to content

Commit b506c2e

Browse files
authored
Merge pull request #102 from numbersprotocol/copilot/fix-css-injection-vulnerability
Fix CSS injection via unvalidated color property in modal
2 parents 635ece7 + 8d4c3c3 commit b506c2e

2 files changed

Lines changed: 69 additions & 5 deletions

File tree

src/modal/modal.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -637,16 +637,22 @@ export class CaptureEyeModal extends LitElement {
637637
);
638638
}
639639

640+
private isValidColor(color: string): boolean {
641+
return CSS.supports('color', color.trim());
642+
}
643+
640644
private updateModalColor() {
641-
// Set primary color
642-
this.style.setProperty('--primary-color', this._color);
645+
// Clear colors first; set only if color is valid
646+
this.style.setProperty('--primary-color', '');
647+
this.style.setProperty('--hover-color', '');
643648

644-
// Set hover color
645-
this.style.setProperty('--hover-color', ''); // Clear the color
646-
if (!this._color) {
649+
if (!this._color || !this.isValidColor(this._color)) {
647650
return;
648651
}
649652

653+
// Set primary color
654+
this.style.setProperty('--primary-color', this._color);
655+
650656
const ctx = document.createElement('canvas').getContext('2d');
651657
if (!ctx) {
652658
return;

src/test/modal_test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,4 +527,62 @@ suite('capture-eye-modal', () => {
527527
buttonCr = el.shadowRoot!.querySelector('.button-content-credentials');
528528
assert.notExists(buttonCr);
529529
});
530+
531+
suite('updateModalColor color validation', () => {
532+
test('sets --primary-color for valid hex color', async () => {
533+
const el = await fixture<CaptureEyeModal>(html`
534+
<capture-eye-modal></capture-eye-modal>
535+
`);
536+
el.updateModalOptions({ nid: '1', color: '#027fe5' });
537+
await el.updateComplete;
538+
assert.equal(el.style.getPropertyValue('--primary-color'), '#027fe5');
539+
});
540+
541+
test('sets --primary-color for valid short hex color', async () => {
542+
const el = await fixture<CaptureEyeModal>(html`
543+
<capture-eye-modal></capture-eye-modal>
544+
`);
545+
el.updateModalOptions({ nid: '1', color: '#fff' });
546+
await el.updateComplete;
547+
assert.equal(el.style.getPropertyValue('--primary-color'), '#fff');
548+
});
549+
550+
test('sets --primary-color for valid named color', async () => {
551+
const el = await fixture<CaptureEyeModal>(html`
552+
<capture-eye-modal></capture-eye-modal>
553+
`);
554+
el.updateModalOptions({ nid: '1', color: 'red' });
555+
await el.updateComplete;
556+
assert.equal(el.style.getPropertyValue('--primary-color'), 'red');
557+
});
558+
559+
test('does not set --primary-color for invalid color with semicolon injection', async () => {
560+
const el = await fixture<CaptureEyeModal>(html`
561+
<capture-eye-modal></capture-eye-modal>
562+
`);
563+
el.updateModalOptions({ nid: '1', color: 'red; --injected: value' });
564+
await el.updateComplete;
565+
assert.equal(el.style.getPropertyValue('--primary-color'), '');
566+
});
567+
568+
test('does not set --primary-color for arbitrary string', async () => {
569+
const el = await fixture<CaptureEyeModal>(html`
570+
<capture-eye-modal></capture-eye-modal>
571+
`);
572+
el.updateModalOptions({ nid: '1', color: 'not-a-color' });
573+
await el.updateComplete;
574+
assert.equal(el.style.getPropertyValue('--primary-color'), '');
575+
});
576+
577+
test('clears --primary-color when color is empty', async () => {
578+
const el = await fixture<CaptureEyeModal>(html`
579+
<capture-eye-modal></capture-eye-modal>
580+
`);
581+
el.updateModalOptions({ nid: '1', color: '#027fe5' });
582+
await el.updateComplete;
583+
el.updateModalOptions({ nid: '1', color: '' });
584+
await el.updateComplete;
585+
assert.equal(el.style.getPropertyValue('--primary-color'), '');
586+
});
587+
});
530588
});

0 commit comments

Comments
 (0)