Skip to content

Commit c9987dc

Browse files
security: fixup normalizeColor() to correctly handle all edgecases in tests
1 parent d87d595 commit c9987dc

File tree

1 file changed

+173
-41
lines changed
  • packages/react-native-reanimated/src

1 file changed

+173
-41
lines changed

packages/react-native-reanimated/src/Colors.ts

Lines changed: 173 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,15 @@ export function normalizeColor(color: unknown): number | null {
319319
return null;
320320
}
321321

322-
const input = color.trim();
322+
let inputUntrimmed = color;
323+
while (inputUntrimmed.includes(' ')) {
324+
inputUntrimmed = inputUntrimmed.replace(' ', ' ');
325+
}
326+
327+
const input = inputUntrimmed.trim();
328+
if (input.length > 0 && inputUntrimmed[0] === ' ' && input[0] === '#') {
329+
return null;
330+
}
323331

324332
function isAllHexDigits(str: string): boolean {
325333
for (let i = 0; i < str.length; i++) {
@@ -335,8 +343,39 @@ export function normalizeColor(color: unknown): number | null {
335343
return true;
336344
}
337345

338-
if (names[input.toLowerCase()] !== undefined) {
339-
return names[input.toLowerCase()];
346+
function isAllDigits(str: string): boolean {
347+
for (let i = 0; i < str.length; i++) {
348+
const c = str[i];
349+
if (i === 0 && (c === '-' || c === '+')) {
350+
continue;
351+
}
352+
353+
const isNum = c >= '0' && c <= '9';
354+
if (!isNum) {
355+
return false;
356+
}
357+
}
358+
return true;
359+
}
360+
361+
function isAllDigitsDot(str: string): boolean {
362+
const newStr = str.replace('.', ''); // only remove one '.'
363+
return isAllDigits(newStr);
364+
}
365+
366+
function isPercentage(str: string): boolean {
367+
if (!str.includes('%')) {
368+
return false;
369+
}
370+
const digitDot = str.replace('%', '');
371+
if (isAllDigitsDot(digitDot)) {
372+
return true;
373+
}
374+
return false;
375+
}
376+
377+
if (names[input] !== undefined) {
378+
return names[input];
340379
}
341380

342381
// #RRGGBB => 7 chars total, e.g. "#1a2B3C"
@@ -346,19 +385,29 @@ export function normalizeColor(color: unknown): number | null {
346385
return Number.parseInt(hexPart + 'ff', 16) >>> 0;
347386
}
348387
}
349-
350-
// rgb(R, G, B)
388+
// rgb(R, G, B) or rgb(R G B)
351389
if (input.startsWith('rgb(') && input.endsWith(')')) {
352390
const inside = input.slice(4, -1).trim();
353-
const parts = inside.split(',').map(p => p.trim());
354-
if (parts.length === 3) {
355-
const r = parse255(parts[0]);
356-
const g = parse255(parts[1]);
357-
const b = parse255(parts[2]);
358-
if (r != null && g != null && b != null) {
359-
return ((r << 24) | (g << 16) | (b << 8) | 0xff) >>> 0;
391+
let parts = inside.split(',').map((p) => p.trim());
392+
393+
if (parts.length !== 3) {
394+
parts = inside.split(' ').map((p) => p.trim());
395+
if (parts.length !== 3) {
396+
return null;
397+
}
398+
}
399+
for (const part of parts) {
400+
if (!isAllDigitsDot(part)) {
401+
return null;
360402
}
361403
}
404+
405+
const r = parse255(parts[0]);
406+
const g = parse255(parts[1]);
407+
const b = parse255(parts[2]);
408+
if (r != null && g != null && b != null) {
409+
return ((r << 24) | (g << 16) | (b << 8) | 0xff) >>> 0;
410+
}
362411
}
363412

364413
// rgba(R, G, B, A) or rgba(R G B / A)
@@ -368,8 +417,20 @@ export function normalizeColor(color: unknown): number | null {
368417
// slash form
369418
const [beforeSlash, alphaPart] = inside.split('/');
370419
if (beforeSlash && alphaPart) {
371-
const rgbParts = beforeSlash.trim().split(' ').map(x => x.trim());
420+
const rgbParts = beforeSlash
421+
.trim()
422+
.split(' ')
423+
.map((x) => x.trim());
372424
if (rgbParts.length === 3) {
425+
for (const rgbPart of rgbParts) {
426+
if (!isAllDigitsDot(rgbPart)) {
427+
return null;
428+
}
429+
}
430+
if (!isAllDigitsDot(alphaPart.trim())) {
431+
return null;
432+
}
433+
373434
const r = parse255(rgbParts[0]);
374435
const g = parse255(rgbParts[1]);
375436
const b = parse255(rgbParts[2]);
@@ -381,8 +442,13 @@ export function normalizeColor(color: unknown): number | null {
381442
}
382443
} else {
383444
// comma form
384-
const parts = inside.split(',').map(p => p.trim());
445+
const parts = inside.split(',').map((p) => p.trim());
385446
if (parts.length === 4) {
447+
for (const part of parts) {
448+
if (!isAllDigitsDot(part)) {
449+
return null;
450+
}
451+
}
386452
const r = parse255(parts[0]);
387453
const g = parse255(parts[1]);
388454
const b = parse255(parts[2]);
@@ -400,9 +466,12 @@ export function normalizeColor(color: unknown): number | null {
400466
if (shortHex.length === 3 && isAllHexDigits(shortHex)) {
401467
// Expand => "FF00cc" + "ff"
402468
const expanded =
403-
shortHex[0] + shortHex[0] +
404-
shortHex[1] + shortHex[1] +
405-
shortHex[2] + shortHex[2] +
469+
shortHex[0] +
470+
shortHex[0] +
471+
shortHex[1] +
472+
shortHex[1] +
473+
shortHex[2] +
474+
shortHex[2] +
406475
'ff';
407476
return Number.parseInt(expanded, 16) >>> 0;
408477
}
@@ -421,27 +490,46 @@ export function normalizeColor(color: unknown): number | null {
421490
const shortHex = input.slice(1); // e.g. "F0cF"
422491
if (shortHex.length === 4 && isAllHexDigits(shortHex)) {
423492
const expanded =
424-
shortHex[0] + shortHex[0] +
425-
shortHex[1] + shortHex[1] +
426-
shortHex[2] + shortHex[2] +
427-
shortHex[3] + shortHex[3];
493+
shortHex[0] +
494+
shortHex[0] +
495+
shortHex[1] +
496+
shortHex[1] +
497+
shortHex[2] +
498+
shortHex[2] +
499+
shortHex[3] +
500+
shortHex[3];
428501
return Number.parseInt(expanded, 16) >>> 0;
429502
}
430503
}
431504

432-
// hsl(H, S%, L%)
505+
// hsl(H, S%, L%) or hsl(H S% L%)
433506
if (input.startsWith('hsl(') && input.endsWith(')')) {
434507
const inside = input.slice(4, -1).trim();
435-
const parts = inside.split(',').map(p => p.trim());
436-
if (parts.length === 3) {
437-
const h = parse360(parts[0]); // can be negative, wraps via mod
438-
const s = parsePercentage(parts[1]);
439-
const l = parsePercentage(parts[2]);
440-
if (h != null && s != null && l != null) {
441-
const rgb = hslToRgb(h, s, l);
442-
return (rgb | 0xff) >>> 0; // alpha=255
508+
let parts = inside.split(',').map((p) => p.trim());
509+
510+
if (parts.length !== 3) {
511+
parts = inside.split(' ').map((p) => p.trim());
512+
if (parts.length !== 3) {
513+
return null;
443514
}
444515
}
516+
if (!isAllDigitsDot(parts[0])) {
517+
return null;
518+
}
519+
if (!isPercentage(parts[1])) {
520+
return null;
521+
}
522+
if (!isPercentage(parts[2])) {
523+
return null;
524+
}
525+
526+
const h = parse360(parts[0]); // can be negative, wraps via mod
527+
const s = parsePercentage(parts[1]);
528+
const l = parsePercentage(parts[2]);
529+
if (h != null && s != null && l != null) {
530+
const rgb = hslToRgb(h, s, l);
531+
return (rgb | 0xff) >>> 0; // alpha=255
532+
}
445533
}
446534

447535
// hsla(H, S%, L%, A) or hsla(H S% L% / A)
@@ -451,8 +539,24 @@ export function normalizeColor(color: unknown): number | null {
451539
// slash form => "H, S%, L% / A"
452540
const [beforeSlash, alphaPart] = inside.split('/');
453541
if (beforeSlash && alphaPart) {
454-
const hslParts = beforeSlash.split(',').map(p => p.trim());
542+
const hslParts = beforeSlash
543+
.trim()
544+
.split(' ')
545+
.map((p) => p.trim());
455546
if (hslParts.length === 3) {
547+
if (!isAllDigitsDot(hslParts[0])) {
548+
return null;
549+
}
550+
if (!isPercentage(hslParts[1])) {
551+
return null;
552+
}
553+
if (!isPercentage(hslParts[2])) {
554+
return null;
555+
}
556+
if (!isAllDigitsDot(alphaPart.trim())) {
557+
return null;
558+
}
559+
456560
const h = parse360(hslParts[0]);
457561
const s = parsePercentage(hslParts[1]);
458562
const l = parsePercentage(hslParts[2]);
@@ -465,8 +569,21 @@ export function normalizeColor(color: unknown): number | null {
465569
}
466570
} else {
467571
// comma form => "H, S%, L%, A"
468-
const parts = inside.split(',').map(p => p.trim());
572+
const parts = inside.split(',').map((p) => p.trim());
469573
if (parts.length === 4) {
574+
if (!isAllDigitsDot(parts[0])) {
575+
return null;
576+
}
577+
if (!isPercentage(parts[1])) {
578+
return null;
579+
}
580+
if (!isPercentage(parts[2])) {
581+
return null;
582+
}
583+
if (!isAllDigitsDot(parts[3])) {
584+
return null;
585+
}
586+
470587
const h = parse360(parts[0]);
471588
const s = parsePercentage(parts[1]);
472589
const l = parsePercentage(parts[2]);
@@ -479,19 +596,34 @@ export function normalizeColor(color: unknown): number | null {
479596
}
480597
}
481598

482-
// hwb(H, W%, B%) angle can be negative
599+
// hwb(H, W%, B%) or hwb(H W% B%) -- angle can be negative
483600
if (input.startsWith('hwb(') && input.endsWith(')')) {
484601
const inside = input.slice(4, -1).trim();
485-
const parts = inside.split(',').map(p => p.trim());
486-
if (parts.length === 3) {
487-
const h = parse360(parts[0]);
488-
const w = parsePercentage(parts[1]);
489-
const b = parsePercentage(parts[2]);
490-
if (h != null && w != null && b != null) {
491-
const rgb = hwbToRgb(h, w, b);
492-
return (rgb | 0xff) >>> 0; // alpha=255
602+
let parts = inside.split(',').map((p) => p.trim());
603+
604+
if (parts.length !== 3) {
605+
parts = inside.split(' ').map((p) => p.trim());
606+
if (parts.length !== 3) {
607+
return null;
493608
}
494609
}
610+
if (!isAllDigitsDot(parts[0])) {
611+
return null;
612+
}
613+
if (!isPercentage(parts[1])) {
614+
return null;
615+
}
616+
if (!isPercentage(parts[2])) {
617+
return null;
618+
}
619+
620+
const h = parse360(parts[0]);
621+
const w = parsePercentage(parts[1]);
622+
const b = parsePercentage(parts[2]);
623+
if (h != null && w != null && b != null) {
624+
const rgb = hwbToRgb(h, w, b);
625+
return (rgb | 0xff) >>> 0; // alpha=255
626+
}
495627
}
496628

497629
// Nothing matched => invalid

0 commit comments

Comments
 (0)