<reponame>CrackerCat/regexploit<filename>regexploit/bin/regexploit_js.py
#!/usr/bin/env python
import argparse
import io
import json
import logging
import os.path
import re
import subprocess
import sys
import traceback
import warnings

from regexploit.ast.sre import SreOpParser
from regexploit.bin.files import file_generator
from regexploit.languages.javascript import fix_js_regex
from regexploit.output.text import TextOutput
from regexploit.redos import find


def handle_line_from_node(line: str, output: TextOutput):
    regex = json.loads(line)
    if pattern := regex.get("pattern"):
        if (pattern_len := len(pattern)) < 5:
            return  # (.+)+
        if pattern_len == 8059 and pattern.startswith("\\u{1F3F4}(?:\\u{E0067"):
            return  # annoying emoji regex
        if pattern.count("*") + pattern.count("+") + pattern.count(",}") < 2:
            return  # no ReDoS possible
        filename = regex["filename"]
        lineno = regex["lineno"]
        try:
            logging.debug("%s#%s: %s", filename, lineno, pattern)
            parsed = SreOpParser().parse_sre(pattern)
        except:
            try:
                fixed = fix_js_regex(pattern)
                re.compile(fixed)
            except:
                print(f"Error parsing: {pattern} from {filename}\n")
                return
            try:
                parsed = SreOpParser().parse_sre(fixed)
            except:
                print(f"Error in regexploit parsing: {pattern} from {filename}")
                print(traceback.format_exc())
                return
        output.next()
        try:
            for redos in find(parsed):
                if redos.starriness > 2:
                    output.record(redos, pattern, filename=filename, lineno=lineno)
        except Exception:
            print(f"Error finding ReDoS: {pattern} from {filename}")
            print(traceback.format_exc())
    elif error := regex.get("error"):
        print("ERR", error, regex.get("filename"))


def process_files(filenames, nodejs_executable, output):
    args = [
        os.path.join(os.path.split(__file__)[0], "javascript", "index.js"),
        *filenames,
    ]
    if nodejs_executable:
        args = [nodejs_executable] + args
    logging.debug("Processing batch: %s", args[2:])
    node = subprocess.Popen(args, stdout=subprocess.PIPE)
    for line in io.TextIOWrapper(node.stdout, encoding="utf-8"):
        handle_line_from_node(line, output)
    rc = node.poll()
    return rc


def main():
    if not os.path.isdir(
        os.path.join(os.path.split(__file__)[0], "javascript", "node_modules")
    ):
        path = os.path.join(os.path.split(__file__)[0], "javascript")
        print("The JavaScript & TypeScript parsers require some node modules.\n")
        print(f"Run (cd {path}; npm install)")
        sys.exit(1)
    with warnings.catch_warnings():
        warnings.simplefilter(
            "ignore", category=FutureWarning
        )  # Some js regexes are weird
        parser = argparse.ArgumentParser(
            description="Parse regexes out of javascript files and scan them for ReDoS"
        )
        parser.add_argument("files", nargs="+", help="Javascript or typescript files")
        parser.add_argument(
            "--node",
            help="Location of nodejs executable (rather than using node from PATH)",
        )
        parser.add_argument(
            "--glob", action="store_true", help="Glob the input filenames (**/*)"
        )
        parser.add_argument("--verbose", action="store_true", help="Verbose logging")
        parser.add_argument(
            "--ignore", action="append", help="Paths containing this string are ignored"
        )
        args = parser.parse_args()

        if args.verbose:
            logging.basicConfig(level=logging.DEBUG)

        output = TextOutput(js_flavour=True)
        files = file_generator(args.files, args.glob, ["*.js", "*.ts"], args.ignore)
        while True:
            batch = []
            for _ in range(50):
                try:
                    batch.append(next(files))
                except StopIteration:
                    if batch:
                        process_files(batch, args.node, output)
                    return
            process_files(batch, args.node, output)
        print(f"Processed {output.regexes} regexes")


if __name__ == "__main__":
    main()

#!/usr/bin/env python
import argparse
import io
import json
import logging
import os.path
import re
import subprocess
import sys
import traceback
import warnings

from regexploit.ast.sre import SreOpParser
from regexploit.bin.files import file_generator
from regexploit.languages.javascript import fix_js_regex
from regexploit.output.text import TextOutput
from regexploit.redos import find


def handle_line_from_node(line: str, output: TextOutput):
    regex = json.loads(line)
    if pattern := regex.get("pattern"):
        if (pattern_len := len(pattern)) < 5:
            return  # (.+)+
        if pattern_len == 8059 and pattern.startswith("\\u{1F3F4}(?:\\u{E0067"):
            return  # annoying emoji regex
        if pattern.count("*") + pattern.count("+") + pattern.count(",}") < 2:
            return  # no ReDoS possible
        filename = regex["filename"]
        lineno = regex["lineno"]
        try:
            logging.debug("%s#%s: %s", filename, lineno, pattern)
            parsed = SreOpParser().parse_sre(pattern)
        except:
            try:
                fixed = fix_js_regex(pattern)
                re.compile(fixed)
            except:
                print(f"Error parsing: {pattern} from {filename}\n")
                return
            try:
                parsed = SreOpParser().parse_sre(fixed)
            except:
                print(f"Error in regexploit parsing: {pattern} from {filename}")
                print(traceback.format_exc())
                return
        output.next()
        try:
            for redos in find(parsed):
                if redos.starriness > 2:
                    output.record(redos, pattern, filename=filename, lineno=lineno)
        except Exception:
            print(f"Error finding ReDoS: {pattern} from {filename}")
            print(traceback.format_exc())
    elif error := regex.get("error"):
        print("ERR", error, regex.get("filename"))


def process_files(filenames, nodejs_executable, output):
    args = [
        os.path.join(os.path.split(__file__)[0], "javascript", "index.js"),
        *filenames,
    ]
    if nodejs_executable:
        args = [nodejs_executable] + args
    logging.debug("Processing batch: %s", args[2:])
    node = subprocess.Popen(args, stdout=subprocess.PIPE)
    for line in io.TextIOWrapper(node.stdout, encoding="utf-8"):
        handle_line_from_node(line, output)
    rc = node.poll()
    return rc


def main():
    if not os.path.isdir(
        os.path.join(os.path.split(__file__)[0], "javascript", "node_modules")
    ):
        path = os.path.join(os.path.split(__file__)[0], "javascript")
        print("The JavaScript & TypeScript parsers require some node modules.\n")
        print(f"Run (cd {path}; npm install)")
        sys.exit(1)
    with warnings.catch_warnings():
        warnings.simplefilter(
            "ignore", category=FutureWarning
        )  # Some js regexes are weird
        parser = argparse.ArgumentParser(
            description="Parse regexes out of javascript files and scan them for ReDoS"
        )
        parser.add_argument("files", nargs="+", help="Javascript or typescript files")
        parser.add_argument(
            "--node",
            help="Location of nodejs executable (rather than using node from PATH)",
        )
        parser.add_argument(
            "--glob", action="store_true", help="Glob the input filenames (**/*)"
        )
        parser.add_argument("--verbose", action="store_true", help="Verbose logging")
        parser.add_argument(
            "--ignore", action="append", help="Paths containing this string are ignored"
        )
        args = parser.parse_args()

        if args.verbose:
            logging.basicConfig(level=logging.DEBUG)

        output = TextOutput(js_flavour=True)
        files = file_generator(args.files, args.glob, ["*.js", "*.ts"], args.ignore)
        while True:
            batch = []
            for _ in range(50):
                try:
                    batch.append(next(files))
                except StopIteration:
                    if batch:
                        process_files(batch, args.node, output)
                    return
            process_files(batch, args.node, output)
        print(f"Processed {output.regexes} regexes")


if __name__ == "__main__":
    main()
#!/usr/bin/env python # (.+)+ # annoying emoji regex # no ReDoS possible #%s: %s", filename, lineno, pattern) # Some js regexes are weird