mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #208] [MERGED] fix(public-repo-hygiene): anchor category-2 host patterns on DNS-label boundaries and allow ports (BE-8729) #216
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#216
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/208
Author: @mattmillerai
Created: 8/22/2026
Status: ✅ Merged
Merged: 8/22/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-8729-hygiene-host-boundaries📝 Commits (6)
33b9f9efix(public-repo-hygiene): anchor category-2 host patterns on DNS-label boundaries and allow ports (BE-8729)85ee416fix(public-repo-hygiene): close the port-backtracking, empty-port and case-folding holes in the category-2 anchors (BE-8729)ce8aa39fix(public-repo-hygiene): repair the right anchor's prose-colon regression, reject userinfo, and state the UTS-46 trade honestly (BE-8729)d6f7966fix(public-repo-hygiene): stop the host-only anchor consuming the port, closing the userinfo family and the prose-colon regressions (BE-8729)17827bbfix(public-repo-hygiene): bound the userinfo run by class and length, closing a quadratic scan and two prose misses (BE-8729)5b0ea79fix(public-repo-hygiene): drop the non-port suffix alternative and repair the unreadable-file crash (BE-8729)📊 Changes
4 files changed (+781 additions, -11 deletions)
View changed files
📝
.github/public-repo-hygiene/README.md(+66 -2)📝
.github/public-repo-hygiene/check_public_repo_hygiene.py(+192 -9)📝
.github/public-repo-hygiene/tests/test_check_public_repo_hygiene.py(+491 -0)📝
.github/workflows/test-public-repo-hygiene.yml(+32 -0)📄 Description
ELI-5
The leak guard has a rule like "flag any link to
posthog.com/project/…". To decide where the host name starts and stops it used\b, the regex word boundary — and\bknows nothing about domain names. A hyphen is not a word character, soevil-posthog.com/project/1looked to it like a perfectly goodposthog.com, and half the rules had no left-hand check at all, sofooslack.com/archives/xandfoonotion.so/pagewere "internal links" too. Meanwhile a real internal link only had to add:443—notion.so:443/team/page— and every rule that expected a/right after the host went silent. This teaches the rules what a domain-name boundary actually is: a dot in front is a genuine subdomain edge and still matches, a letter/digit/hyphen in front means a different domain entirely, and an explicit port is no longer an escape hatch.What changed
INTERNAL_MARKER_RES(check_public_repo_hygiene.py) — two new shared fragments and every host pattern rebuilt on them:_HOST_L = (?<![A-Za-z0-9-])— a left anchor on the real DNS-label alphabet. It deliberately permits a preceding dot, because a dot is the one character that can precede a host and still leave it that host.(?<![A-Za-z0-9.-])is the tempting-but-wrong tightening: it would reject every subdomain-prefixed positive the existing fixtures pin (www.notion.so,comfy.notion.site,comfy.slack.com/archives,us.posthog.com/project). There is an in-file comment saying so._PORT = (?::\d*)?— an explicit port between host and path. The digits are*, not+:port = *DIGITin RFC 3986, so the empty porthttps://notion.so:/pageis a valid URL for the same host and was the same one-token bypass:443was._HOST_FLAGS = re.IGNORECASE | re.ASCIIon the eight host patterns (incident-\d+is not a host and keeps plainre.IGNORECASE). The ignore-case flag alone folds Unicode, and review was right that this is two boundaries, not one:i, solınear.app/xandnotİon.so/pageread as the real hosts — but UTS-46 leaves U+0131 alone and maps U+0130 toi+ a combining dot, so neither resolves anywhere near the real service. Flagging them was a false positive;re.ASCIIremoves it, restoring the invariantREPO_REF_REalready scopes its own flag to keep.sandk, soſlack.com/archives/C123really does resolve toslack.com.re.ASCIIturns those into misses. That is a deliberate scope call — an obfuscated spelling, in the same family as the punycode/percent-encoded/defanged spellings already listed under "Known limitations" — and the code comment, the test name and the README all now say so rather than claiming a domain nobody else can reach.app.slack.comis the only host-only pattern (nothing requires a/after it), so it loses\bfor a right anchor of its own — and, after four review rounds, it does not consume_PORTat all and carries only three alternatives:An optional, greedy port sitting in front of a negative lookahead backtracks — the engine hands digits back one at a time until the lookahead is satisfied — so every alternative had to be correct for every possible port split. That cost two rounds: round 2 shipped a hole (
app.slack.com:443.evil.commatched anyway), its fix shipped a regression (app.slack.com:generalwent silent), and that fix relocated it (app.slack.com:2FA). Asearch()only needs a boolean, so nothing has to be consumed; one lookahead reads the whole tail, the port never enters the match, and each alternative is independent:\.?[A-Za-z0-9-]— a following label (app.slack.com.evil.com), what\bused to accept. A dot is allowed when what follows is not a label, soapp.slack.com./xstill matches.@— the userinfo delimiter; inhttps://app.slack.com@evil.com/the real host isevil.com, the canonical phishing shape.[.:][A-Za-z0-9._~%:-]{0,64}@— the same with userinfo in between (:@,:secret@,.@, and:443.evil.com@), all of which an adjacent-only@left flagged. The class is the characters a real credential uses, not "anything but a URL delimiter": the first cut,[^\s/?#@]*, crossed commas, quotes and braces, so any later@on the line silenced the host. It is length-bounded because an unbounded run made a long single line quadratic to scan. Both bounds cost an over-flag, and both are now named in "Known limitations" and pinned: a colon-chained run still reaches a later@(:has to stay in the class, since:user:pass@is real userinfo), and userinfo past 64 characters is out of the run's reach. A leadingadmin@app.slack.comstill matches. Every other pattern's required/directly after host+port already blocks the suffix shape —notion.so.evil.com/pageandlinear.app.evil.com/xare non-matches before and after this change, and that is asserted.:\d+\.[A-Za-z0-9-]to rejectapp.slack.com:443.evil.comas "a port the host continues past". Round 5 removed it:port = *DIGIT, so WHATWG's port state fails on the., the URL does not parse at all,urlsplit(…).hostnameisapp.slack.com, and curl and Go'snet/urlboth error on the port. No mainstream parser resolves that line toevil.com, so the only readable host on it is the internal one — suppressing it was a miss, not a false-positive fix, and it also caused the:2.5/:1.0.1prose misses that shared the shape. The genuine phishing form,app.slack.com:443.evil.com@evil.com, is rejected by the userinfo alternative and needed nothing here (asserted both ways).InternalMarkerCategoryTest: 5 third-party lookalike hosts + 2 same-namespace neighbours, 3 ported URLs, 3 empty-port URLs, 8 userinfo shapes (5 rejected, 3 prose forms that must survive), 2 different-host homoglyphs, 2 UTS-46-mapped spellings pinned as out-of-scope misses, the IDN-neighbour limitation (both boundaries), the host-only trailing-root-label match, 7 host-only right-boundary shapes, the four non-numeric-port lines that must now be flagged plus their two userinfo forms that must not, the colon-chained userinfo miss, both sides of the 64-character bound, and the label-adjacency misses.test_each_marker_is_flaggedis untouched and green, which is what pins the subdomain-prefixed positives.CoverageReportingTestgainstest_an_unopenable_file_warns_instead_of_crashing(see below).test-public-repo-hygiene.ymlgains lookalike hosts (a homoglyph domain and two userinfo URLs) in the clean-repo fixture, and ported-URL + empty-port cases in the per-category dirty-repo fixture. Both were verified to fail against the pre-change checker (lookalikes: old exit 1 / new exit 0; ported URL: old exit 0 / new exit 1), so they are real pins rather than decoration. Round 5 moved the bareapp.slack.com:443.evil.com/xline out of the clean fixture — it is a real reference and is now correctly flagged — and replaced it with its userinfo form, which is still rejected._read_text'sopen()-failure path returned a five-tuple where the docstring, every sibling branch and its only caller use four, so any unreadable tracked file (EACCES on a mode-000 blob, EMFILE, an I/O error) raisedValueError: too many values to unpack (expected 4)and exited 1 — whichpublic-repo-hygiene.ymlrenders as internal-only references found, with no finding listed, on a repo that is in fact clean. It predates this branch (landed with the centralization). The existing coverage test reaches thelstatfailure path only, which is why nothing caught it; the new test uses a mode-000 regular file solstatsucceeds and only the read fails, and it reproduces theValueErrorwhen the fifth value is put back.Falsification, not just unit tests
Tests I author only assert my own premise, and the user-facing effect of this diff is that six line shapes stop being findings. So the premise — "these are unrelated third-party domains, not the internal services" — was checked read-only against DNS rather than assumed:
slack.com54.71.95.193ns-1493.awsdns-58.org.fooslack.composthog.com216.150.1.1julissa.ns.cloudflare.com.evil-posthog.comlinear.app172.64.147.211tim.ns.cloudflare.com.my-linear.appnotion.so208.103.161.1dana.ns.cloudflare.com.foonotion.soapp.slack.com35.81.85.251app.slack.com.evil.comNot one of the six lookalikes resolves at all, and none is a delegation of the real service's zone. They are separate registrable names, which is exactly what the DNS label alphabet says they are: labels are dot-separated and made of letters, digits and hyphens, so a letter/digit/hyphen in front of
posthog.comis part of a different label, while a dot in front is a delegation edge. The direction the diff moves detection is therefore: strictly fewer lookalike-third-party-host lines flagged (false positives removed), strictly more ported-URL lines flagged (false negatives closed).Corpus sweep — both halves
Old and new pattern sets run over the pre-change
maintree, all 175 tracked files (this repo is deliberately not self-enrolled, so this is a stress corpus rather than a clean one):posthog.com/docs).https://notion.so./page, the same host to any resolver — still walks past every category-2 pattern, because they require/(or a port then/) immediately after the host. Same for obfuscated spellings: percent-encoding (%2E), punycode (xn--), defanging (notion[.]so). All of these were misses before this change too and are unchanged by it; corpus occurrences on this tree: 0. They are now written down in the checker README's "Known limitations" rather than left implicit. Deliberately not fixed here: the ticket specifies an exact pattern set validated as a unit upstream, and quietly widening it would leave the shipped set unvalidated.Detection parity with the SDK scripts
The 18/18 findings-parity proof from BE-8654 no longer describes this branch, by design and for the second time (PR #207 shipped a detection change the same way and is the precedent). Direction of divergence, stated plainly: the checker now flags strictly fewer lookalike-third-party-host lines and strictly more ported-URL lines than the two per-repo scripts the centralization retired. Neither SDK copy is touched here — editing them is what would invalidate the parity proof that licenses their deletion.
Judgment calls / criteria not met as written
UTF-16/UTF-32toTICKET_ALLOWLIST) is not in this diff, because it is already satisfied onmainby a better mechanism. The ticket was written from a spike snapshot that predates the BE-8654 review, which addedTICKET_ALLOWED_PREFIXES(CVE,CWE,PEP,RFC,ISO,UTF).UTF-16andUTF-32therefore already clear by prefix, andtest_public_identifier_namespaces_clear_by_prefixalready pins both by name. Adding two exact entries next toUTF-8would be dead list content. The ticket's follow-on request — update the comment attest_an_excluded_path_may_carry_a_work_tree_encoding_attributeand restore its fixture to canonicalUTF-16casing — is likewise already done onmain; the comment there already citesTICKET_ALLOWED_PREFIXESand the fixture already readsUTF-16.UTF-99negative control was deliberately not added. It contradicts the shipped design:UTF-99splits to the prefixUTF, which is allowlisted, so it clears and is not a finding. Asserting otherwise would pin a behaviour this checker does not have and never agreed to have. The analogous control that is correct for a prefix allowlist already exists —test_an_allowlisted_prefix_does_not_clear_a_longer_acronym(CVEX-1234,ISOP-99).Ran 74 testsis stale —mainis at 97 (OK (skipped=1)), for the same snapshot reason. This branch is 100.https://énotion.so/page— a different registrable domain — reports asnotion.so. The blunt fix, a second lookbehind rejecting any non-ASCII character, would also silence a real link written straight after a curly quote, an em dash or CJK prose, and a missed leak costs more than an extra finding in a leak guard. Pinned bytest_an_idn_neighbour_still_reports_as_the_real_hostand written down in the README so changing it is deliberate.app.slack.com:443.evil.com, in both directions, and round 5 settles it. Round 2 let it match and this body defended that as a safe over-flag; rounds 3–4 rejected it with:\d+\.[A-Za-z0-9-]and this body defended that, calling the resulting:2.5miss "a miss for a miss, in the direction that flags less". Both readings were wrong, and the second was wrong twice: a non-numeric port makes the URL unparseable, so the line's only readable host is the internal one and suppressing it is a plain miss — and removing an alternative from a negative lookahead can only ever flag MORE, never less, which is the same rule stated twenty lines below it in the same file. The alternative is gone; both directions are now asserted (the bare line flagged, the userinfo form rejected)._as a host character, the right-hand half of the IDN-neighbour limitation, and label characters adjacent to a three-label host (my-app.slack.com,app.slack.com-hosted). Each is a real over-flag or miss in the safe direction whose only fix trades it for the opposite error; every regex tweak in this PR has cost a review round, so they are written into README "Known limitations" and pinned by tests instead of re-tuned blind. The last one also scopes the file's own rationale: "a letter, digit or hyphen in front means a different registrable name" is exact for the two-label patterns and over-claims for the three-label ones, where it is the same customer-sub-domain shape the README already documents for Datadog.\to/for special schemes, sohttps://notion.so\pageis hostnotion.so) and a Datadog custom organization sub-domain (comfyapp.datadoghq.com/dashboard/1— Datadog hands customers their own<name>.datadoghq.com, so unlike*.google.comthat namespace is not vendor-only) are both real misses. Neither is the accidental-paste shape this guard is scoped to, and both would widen what the shipped pattern set matches beyond what was validated. They are in README "Known limitations" so a deliberate widening has something to start from. The Datadog case also cost a fixture:myapp.datadoghq.comwas removed fromSAME_NAMESPACE_HOSTSrather than relabelled, because pinning it as a correct non-finding would assert that a custom-sub-domain dashboard should be missed.Unexercised artifacts
Verification
python3 -m unittest discover -s .github/public-repo-hygiene/tests -p 'test_*.py'— 114 passed, 0 failed, 1 skipped (110 before round 5). Five review rounds; every round is recorded in the in-file comments rather than only in this body.mainbaseline is 97 with the same single skip (pre-existing: APFS rejects the non-UTF-8 filename that test builds; it is Linux-only by design).run:smoke steps fromtest-public-repo-hygiene.ymlexecuted locally underbash -e— all pass, including the two new cases._read_textregression test re-run against a copy with the fifth tuple value restored — reproducesValueError: too many values to unpack (expected 4, got 5), so it pins the fix rather than merely passing beside it.maintree — 22 → 22 category-2 findings, 0 lines changed.python3 .github/workflow-pins/check_workflow_pins.py— OK, 11 workflows, no default, every ref checkout guarded.python3 .github/agents-md-integrity/check_agents_md.py --root .fails onAGENTS.mdbeing 305 lines against a 200-line ceiling. Pre-existing and unrelated — identical failure on unmodifiedmain, this diff does not touchAGENTS.md, and this repo carries no self-enrolled agents-md caller.Refs BE-8729 — deliberately not
Closes. The body above names an unfixed detection gap (the trailing-root-label host, plus percent-encoded / punycode / defanged spellings) onINTERNAL_MARKER_RES, the exact artifact this ticket names, and two of the ticket's own acceptance items were resolved as already-done-or-contradicted rather than implemented as written. The ticket should stay open to carry that residual and to confirm those two dispositions.Provenance
run:steps oftest-public-repo-hygiene.ymlreplayed locally underbash -e, all pass, including the moved clean-repo lookalike fixture and the ported/empty-port dirty fixtures; a 27-case FP/FN matrix run directly against the compiled round-5 host-only pattern, 0 mismatches (17 positives incl. every prose-colon shape rounds 2–3 lost,admin@app.slack.com, the four comma/quote/brace/NBSP prose forms, and the four non-numeric-port lines round 5 recovered; 10 negatives incl.app.slack.com.evil.com, the five userinfo shapes, both userinfo forms of:443.evil.com, and the two label-adjacency misses); a round-4-vs-round-5 corpus sweep over 175 tracked files of main (19 → 19 category-2 findings, zero lines newly silent in either direction); and the new_read_textregression test re-run against a copy with the five-tuple restored, which reproducesValueError: too many values to unpack (expected 4, got 5). Earlier revisions additionally ran 37-case and 52-case FP/FN matrices, an old-vs-new corpus sweep (22 → 22, 0 changed), read-only DNS resolution of 10 real-vs-lookalike hosts, and the workflow-pins lint (11 workflows).UTF-16/UTF-32toTICKET_ALLOWLIST) was not implemented because it is already satisfied on main byTICKET_ALLOWED_PREFIXES, and its requestedUTF-99negative control was not added because it contradicts that shipped design; both are argued above. Change 1 and all of the requested test coverage shipped as specified. Across five review rounds, twenty-six findings: eighteen fixed in code and eight deliberately documented as limitations rather than fixed — the IDN neighbour on both boundaries, ASCII_as a host character, the backslash separator, the Datadog custom organization sub-domain, label characters adjacent to a three-label host, the colon-chained userinfo run, and the 64-character userinfo bound's over-flag. Each trades a rare error for its opposite, or widens the shipped pattern set past what was validated; all are in README "Known limitations" with the reasoning, and every one is pinned by a test so changing it is deliberate. Round 5 also reversed a round-3/4 code decision (the:\d+\.[A-Za-z0-9-]alternative) and fixed one pre-existing crash in_read_textthat is outside this PR's stated scope — a one-token typo whose blast radius is a false "references found" failure on any repo with an unreadable tracked file.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.