Fixed upstream 27 Oct 2026  Three privately reported fixes in ai-job-search

ai-job-search: a slow robots.txt rule, a loose curl call and a gitignore guard git disagreed with

ai-job-search is a job-application framework built on Claude Code: it evaluates postings, tailors CVs and writes cover letters, and users fork it and keep their own data in it. It has about 67,000 stars and 21,000 forks on GitHub. A scan of the tree on 19 Oct 2026 flagged three issues in its helper tools, and we reported each one privately through GitHub with a proof of concept and a proposed fix. Maintainer Mads Lorentzen (MadsLorentzen) confirmed all three on 27 Oct, rated them lower than we had, and fixed them on master the same day, crediting us in the commit messages. The ReDoS fix is the matcher we proposed.

1. A robots.txt rule could stall the check for minutes

tools/robots_check.py fetches a job site’s robots.txt before the agent reads a posting there. Its _match turned every * in a rule into a regular-expression .*:

pat = pattern[:-1] if end_anchor else pattern
rx = '^' + ''.join('.*' if c == '*' else re.escape(c) for c in pat) + ('$' if end_anchor else '')
return len(pattern) if re.match(rx, path) else -1

The file comes from the site being checked. A rule with many wildcard segments that does not match the path, such as Disallow: /*a*a*a…*b$, makes Python’s backtracking regex engine take exponential time. That happens in the main process after curl has returned, so curl’s own time limit does not help.

We proposed a matcher that walks a table instead of using a regex, in time proportional to the pattern length times the path length. Mads Lorentzen measured 5.6 seconds at 14 segments on current master and took our matcher into pull request #779. His commit message says a 200,000-case differential fuzz against the old regex matcher found no change in answers, and three new tests pin the prefix, anchored and slash-spanning cases.

2. The same tool passed any URL scheme to curl

gate() rebuilt the target as scheme://host/robots.txt without checking the scheme, and _fetch() ran curl -L without limiting protocols. A direct call with gopher: or ftp: reached curl as is, and a redirect could switch protocols.

The maintainer showed the reach was narrower than our report said. The rebuilt URL turns file:///etc/passwd into file:///robots.txt, so no file is read; curl already refuses to follow a redirect to file, scp or smb; the response body is never printed; and the main fetch path, fetch_page.py, already allowed only http(s). What was left is a blind request plus an HTTP status. He still took the fix we proposed in the same pull request: gate() now fails closed on any scheme but http(s) or a URL without a host, and curl gets --proto =http,https --proto-redir =http,https.

3. The personal-data guard read .gitignore differently from git

Forks of the project hold salary data, CVs and scraped job lists, and the project’s SECURITY.md promises that a CI job fails any change that weakens the .gitignore rules for them. check_gitignore() in tools/security_guards.py did this by comparing lines of the root .gitignore after stripping them:

lines = [line.strip() for line in path.read_text(encoding="utf-8").splitlines()]

Git ignores only unescaped trailing whitespace. A leading space is part of the pattern, so  salary_data.json no longer ignores salary_data.json, while the guard stripped the space and passed. The guard also read only the root file, so a .gitignore in a subfolder with a ! negation could re-include personal files with CI green.

Mads Lorentzen reproduced both parts and rated the issue low, since either change shows up in a reviewed diff, but added:

“catching exactly that is the guard’s job.”

The fix in commit 0bcfd75 reads lines the way git does and checks every .gitignore below the root for negations, with an allowlist for intentional ones. SECURITY.md now states both rules.

Timeline

How to read this

None of the three is a remote hole. The ReDoS hangs one local check that a timeout or the user can stop. The curl issue needs a direct call and gives a blind request. The gitignore bypass needs a change to a tracked file that a reviewer would see. The maintainer rated each one lower than our triage did, and his notes made the reports more accurate. The value is in tools that do what the project’s security notes promise. Findings are leads for people, not verdicts.

Sources

← Back to Success Stories