Skip to content

Build1 publisher3 min readPublished

A read-only robots.txt dated two years before the domain passed three monitors

A cloaker left a plain robots.txt in the web root of utilorax.com. Apache's !-f rule let it win over the generated one, and every instrument built after the first hack had its own reason to call the result healthy.

The Engineer · Build desk

Illustration accompanying A read-only robots.txt dated two years before the domain passed three monitors

What happened

  • For six days a morning health check emailed the same green line: the sitemap index held 54 child sitemaps and 6,240 URLs, robots.txt pointed at a sitemap, and every page earning impressions answered 200.
  • The robots.txt actually being served carried seven Sitemap lines, none of them the site's own, one of them pointing through a goods.php URL.
  • The check that passed was a single str_contains test for the string "Sitemap:", so a file supplied entirely by the attacker satisfied it.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint A presence test buys coverage against deletion and none against replacement, because the attacker satisfies the predicate as easily as the owner does.
  • decision A hash baseline is only as trustworthy as the cleanup that preceded it, so the safe order is to rebuild the file set from git and then hash, not to snapshot whatever is on disk.
  • exposure Any site that generates routes through a front controller behind a !-f rule can have those routes overridden one file at a time, and the override raises nothing to notice.
  • precedent Reporting a failed fetch as not evaluated today rather than as a pass is the minimum any network-reaching check needs before its green line means anything.

Apache resolves this before any of the site's own code runs. The .htaccess rules send a request to index.php only when the requested path is not a real file and not a real directory [10]. index.php generates robots.txt, so the Sitemap line always matches the host that served it [9]. Drop a plain robots.txt into the web root and requests for that path stop reaching the generator, silently, because that is what !-f means [11]. There is no conflict to notice [11].

The morning check fetched the file and looked for one substring, `Sitemap:` [6]. The planted file mentioned seven sitemaps, so it passed with a margin [7]. "This is the same bug as a login check that verifies a password field is non-empty," the site's owner wrote on dev.to [8]. The test had been written against the failure the owner imagined, an accidental deletion of the Sitemap line; the failure that happened was a deliberate replacement [20].

The file on disk was 356 bytes, mode 444, and dated 7 July 2024 [12]. The domain was registered in July 2026, which puts the timestamp roughly two years before the project existed [1]. Read-only meant a careless cleanup would fail, and it was the only file outside `vendor/` set to 444 [12]. One of the seven foreign sitemap URLs went through `goods.php`, the shape of a Japanese shop-spam campaign that had already cloaked the site, serving Googlebot a clothing store while people saw the real pages [4]. The owner cleaned that up on 5 September and recorded the site as clean [5].

Three checks had been built after that first hack, and each waved the leftover through for a different reason [13]. The integrity monitor hashes every static file and reports changes, and its baseline was taken after the cleanup, so the planted robots.txt entered it as known-good [14]. From then on the monitor's job was to guarantee the attacker's file stayed exactly as they left it, and it did that perfectly [14]. "A baseline taken on a compromised system does not detect the compromise. It certifies it," the owner wrote [15].

The third instrument was right. The off-server verifier compares live files against git and lists anything on the server git does not know about, and on 5 September it listed robots.txt among caches, a ratings database and a speed-test payload, where it read as runtime clutter [16]. It was the only entry on that list that could not legitimately exist, because the real robots.txt is generated and no file of that name belongs on disk at all [17].

That last point is what makes the new rule safe here and not everywhere. The rewritten check pulls every Sitemap URL out with `preg_match_all`, compares each against the site's own sitemap URL, and reports both a missing own entry and any foreign entry; separately, it treats the existence of a static robots.txt as a finding, on the grounds that it is shadowing the dynamic one [18]. The existence half only holds because the route is generated [17]. The ownership half transfers to any site: a predicate the attacker can satisfy tells you nothing about whose sitemap you are advertising [6]. The verifier now keeps shadowing static files in their own section, and a failed fetch is reported as not evaluated today [19].

What to watch

  • Whether anything else the cloaker left on disk also went into the post-cleanup hash baseline as known-good.
  • Whether the shadowing test gets extended to the other paths index.php generates, including sitemap*.xml.
  • Whether the seven foreign sitemap URLs were fetched by crawlers during the six days the check read green.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories