close
Skip to content

Commit 4196fcf

Browse files
committed
path: unwind regular expressions in POSIX
This is the first part to removing REDOS vulnerabilities from v4.x The function `splitPathRe` exposed a REDOS vulnerability. It was only utilized in the POSIX implementation of a number of the path utilities. In v6.x a change landed that unwound this regular expression, and in turn patched the vulnerability. This commit copies the unwound implementation currently found on v8.x. It is completely self contained. I attempted to keep all warnings and deprecations the same as the v4.x implementation, but may have missed something buried in the large unwound functions. Refs: b212be08f6
1 parent b39ba55 commit 4196fcf

1 file changed

Lines changed: 220 additions & 38 deletions

File tree

‎lib/path.js‎

Lines changed: 220 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -399,21 +399,8 @@ win32.parse = function(pathString) {
399399
win32.sep = '\\';
400400
win32.delimiter = ';';
401401

402-
403-
// Split a filename into [root, dir, basename, ext], unix version
404-
// 'root' is just a slash, or nothing.
405-
const splitPathRe =
406-
/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
407402
var posix = {};
408403

409-
410-
function posixSplitPath(filename) {
411-
const out = splitPathRe.exec(filename);
412-
out.shift();
413-
return out;
414-
}
415-
416-
417404
// path.resolve([from ...], to)
418405
// posix version
419406
posix.resolve = function() {
@@ -527,39 +514,159 @@ posix._makeLong = function(path) {
527514

528515

529516
posix.dirname = function(path) {
530-
const result = posixSplitPath(path);
531-
const root = result[0];
532-
var dir = result[1];
533-
534-
if (!root && !dir) {
535-
// No dirname whatsoever
517+
if (path.length === 0)
536518
return '.';
519+
var code = path.charCodeAt(0);
520+
var hasRoot = (code === 47);
521+
var end = -1;
522+
var matchedSlash = true;
523+
for (var i = path.length - 1; i >= 1; --i) {
524+
code = path.charCodeAt(i);
525+
if (code === 47) {
526+
if (!matchedSlash) {
527+
end = i;
528+
break;
529+
}
530+
} else {
531+
// We saw the first non-path separator
532+
matchedSlash = false;
533+
}
537534
}
538535

539-
if (dir) {
540-
// It has a dirname, strip trailing slash
541-
dir = dir.substr(0, dir.length - 1);
542-
}
543-
544-
return root + dir;
536+
if (end === -1)
537+
return hasRoot ? '/' : '.';
538+
if (hasRoot && end === 1)
539+
return '//';
540+
return path.slice(0, end);
545541
};
546542

547543

548544
posix.basename = function(path, ext) {
549545
if (ext !== undefined && typeof ext !== 'string')
550546
throw new TypeError('ext must be a string');
551547

552-
var f = posixSplitPath(path)[2];
548+
var start = 0;
549+
var end = -1;
550+
var matchedSlash = true;
551+
var i;
552+
553+
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
554+
if (ext.length === path.length && ext === path)
555+
return '';
556+
var extIdx = ext.length - 1;
557+
var firstNonSlashEnd = -1;
558+
for (i = path.length - 1; i >= 0; --i) {
559+
const code = path.charCodeAt(i);
560+
if (code === 47/*/*/) {
561+
// If we reached a path separator that was not part of a set of path
562+
// separators at the end of the string, stop now
563+
if (!matchedSlash) {
564+
start = i + 1;
565+
break;
566+
}
567+
} else {
568+
if (firstNonSlashEnd === -1) {
569+
// We saw the first non-path separator, remember this index in case
570+
// we need it if the extension ends up not matching
571+
matchedSlash = false;
572+
firstNonSlashEnd = i + 1;
573+
}
574+
if (extIdx >= 0) {
575+
// Try to match the explicit extension
576+
if (code === ext.charCodeAt(extIdx)) {
577+
if (--extIdx === -1) {
578+
// We matched the extension, so mark this as the end of our path
579+
// component
580+
end = i;
581+
}
582+
} else {
583+
// Extension does not match, so our result is the entire path
584+
// component
585+
extIdx = -1;
586+
end = firstNonSlashEnd;
587+
}
588+
}
589+
}
590+
}
591+
592+
if (start === end)
593+
end = firstNonSlashEnd;
594+
else if (end === -1)
595+
end = path.length;
596+
return path.slice(start, end);
597+
} else {
598+
for (i = path.length - 1; i >= 0; --i) {
599+
if (path.charCodeAt(i) === 47/*/*/) {
600+
// If we reached a path separator that was not part of a set of path
601+
// separators at the end of the string, stop now
602+
if (!matchedSlash) {
603+
start = i + 1;
604+
break;
605+
}
606+
} else if (end === -1) {
607+
// We saw the first non-path separator, mark this as the end of our
608+
// path component
609+
matchedSlash = false;
610+
end = i + 1;
611+
}
612+
}
553613

554-
if (ext && f.substr(-1 * ext.length) === ext) {
555-
f = f.substr(0, f.length - ext.length);
614+
if (end === -1)
615+
return '';
616+
return path.slice(start, end);
556617
}
557-
return f;
558618
};
559619

560620

561621
posix.extname = function(path) {
562-
return posixSplitPath(path)[3];
622+
var startDot = -1;
623+
var startPart = 0;
624+
var end = -1;
625+
var matchedSlash = true;
626+
// Track the state of characters (if any) we see before our first dot and
627+
// after any path separator we find
628+
var preDotState = 0;
629+
for (var i = path.length - 1; i >= 0; --i) {
630+
const code = path.charCodeAt(i);
631+
if (code === 47) {
632+
// If we reached a path separator that was not part of a set of path
633+
// separators at the end of the string, stop now
634+
if (!matchedSlash) {
635+
startPart = i + 1;
636+
break;
637+
}
638+
continue;
639+
}
640+
if (end === -1) {
641+
// We saw the first non-path separator, mark this as the end of our
642+
// extension
643+
matchedSlash = false;
644+
end = i + 1;
645+
}
646+
if (code === 46) {
647+
// If this is our first dot, mark it as the start of our extension
648+
if (startDot === -1)
649+
startDot = i;
650+
else if (preDotState !== 1)
651+
preDotState = 1;
652+
} else if (startDot !== -1) {
653+
// We saw a non-dot and non-path separator before our dot, so we should
654+
// have a good chance at having a non-empty extension
655+
preDotState = -1;
656+
}
657+
}
658+
659+
if (startDot === -1 ||
660+
end === -1 ||
661+
// We saw a non-dot character immediately before the dot
662+
preDotState === 0 ||
663+
// The (right-most) trimmed path component is exactly '..'
664+
(preDotState === 1 &&
665+
startDot === end - 1 &&
666+
startDot === startPart + 1)) {
667+
return '';
668+
}
669+
return path.slice(startDot, end);
563670
};
564671

565672

@@ -587,15 +694,90 @@ posix.format = function(pathObject) {
587694

588695
posix.parse = function(pathString) {
589696
assertPath(pathString);
697+
var ret = { root: '', dir: '', base: '', ext: '', name: '' };
698+
if (pathString.length === 0)
699+
return ret;
700+
var code = pathString.charCodeAt(0);
701+
var isAbsolute = (code === 47);
702+
var start;
703+
if (isAbsolute) {
704+
ret.root = '/';
705+
start = 1;
706+
} else {
707+
start = 0;
708+
}
709+
var startDot = -1;
710+
var startPart = 0;
711+
var end = -1;
712+
var matchedSlash = true;
713+
var i = pathString.length - 1;
714+
715+
// Track the state of characters (if any) we see before our first dot and
716+
// after any path separator we find
717+
var preDotState = 0;
718+
719+
// Get non-dir info
720+
for (; i >= start; --i) {
721+
code = pathString.charCodeAt(i);
722+
if (code === 47) {
723+
// If we reached a path separator that was not part of a set of path
724+
// separators at the end of the string, stop now
725+
if (!matchedSlash) {
726+
startPart = i + 1;
727+
break;
728+
}
729+
continue;
730+
}
731+
if (end === -1) {
732+
// We saw the first non-path separator, mark this as the end of our
733+
// extension
734+
matchedSlash = false;
735+
end = i + 1;
736+
}
737+
if (code === 46) {
738+
// If this is our first dot, mark it as the start of our extension
739+
if (startDot === -1)
740+
startDot = i;
741+
else if (preDotState !== 1)
742+
preDotState = 1;
743+
} else if (startDot !== -1) {
744+
// We saw a non-dot and non-path separator before our dot, so we should
745+
// have a good chance at having a non-empty extension
746+
preDotState = -1;
747+
}
748+
}
590749

591-
var allParts = posixSplitPath(pathString);
592-
return {
593-
root: allParts[0],
594-
dir: allParts[0] + allParts[1].slice(0, -1),
595-
base: allParts[2],
596-
ext: allParts[3],
597-
name: allParts[2].slice(0, allParts[2].length - allParts[3].length)
598-
};
750+
if (startDot === -1 ||
751+
end === -1 ||
752+
// We saw a non-dot character immediately before the dot
753+
preDotState === 0 ||
754+
// The (right-most) trimmed path component is exactly '..'
755+
(preDotState === 1 &&
756+
startDot === end - 1 &&
757+
startDot === startPart + 1)) {
758+
if (end !== -1) {
759+
if (startPart === 0 && isAbsolute)
760+
ret.base = ret.name = pathString.slice(1, end);
761+
else
762+
ret.base = ret.name = pathString.slice(startPart, end);
763+
}
764+
} else {
765+
if (startPart === 0 && isAbsolute) {
766+
ret.name = pathString.slice(1, startDot);
767+
ret.base = pathString.slice(1, end);
768+
} else {
769+
ret.name = pathString.slice(startPart, startDot);
770+
ret.base = pathString.slice(startPart, end);
771+
}
772+
ret.ext = pathString.slice(startDot, end);
773+
}
774+
775+
if (startPart > 0)
776+
ret.dir = pathString.slice(0, startPart - 1);
777+
else if (isAbsolute)
778+
ret.dir = '/';
779+
780+
return ret;
599781
};
600782

601783

0 commit comments

Comments
 (0)