Issue Nº 54 — Nov 3, 2026

PurgoMalum masks "Scunthorpe" and reports every error as HTTP 200

Issue fifty-four covers PurgoMalum in the Data Validation category. It is a profanity filter that needs no key and sends a wildcard CORS header. We queried it live on 3 November 2026. It turned the town name Scunthorpe into S****horpe. It also returns each error as a 200 response with an error key. Check the body, not the status code.

The Scunthorpe problem

We sent GET https://www.purgomalum.com/service/json?text=Scunthorpe. The body was {"result":"S****horpe"}. GET /service/containsprofanity?text=Scunthorpe returned true.

A longer test sentence was Scunthorpe assassin classic cockpit. The result was {"result":"S****horpe assassin classic cockpit"}. The filter masked only the town name. It left assassin, classic and cockpit alone.

So the default list matches inside some words and not inside others. Do not use the filter to block names or place names without a human review step.

What the default list catches

text=this is shit returned {"result":"this is ****"}. text=this is crap came back unchanged, so crap is not on the default list.

Some spelling tricks work. GET /service/containsprofanity?text=sh1t returned true. text=s.h.i.t returned {"result":"*******"}, which masks the dots too. text=Sh!t SHIT shitty returned {"result":"**** **** ******"}. The filter ignores case, and it masked the whole word shitty.

Your own words go in the add parameter. With add=crap, the text this is crap returned {"result":"this is ****"}. The add list matches whole words. With add=ass, the text assassin class stayed unchanged.

Errors arrive as HTTP 200

An empty text parameter returned HTTP 200 and {"error":"No Input"}. We saw the same status for every error below.

Eleven words in add (a to k) returned {"error":"User Black List Exceeds Limit of 10 Words."}. A 22-character fill_text returned {"error":"User Replacement Text Exceeds Limit of 20 Characters."}.

The add and fill_text limits also skip short text. text=x with the same eleven add words returned {"result":"x"}. text=x with the 22-character fill_text did the same.

An unknown path is different. GET /service/foo returned HTTP 404. Test for the error key in the JSON before you read result.

fill_char accepts only six symbols

The documentation lists underscore, tilde, hyphen, equal sign, pipe and asterisk for fill_char. We tried four of them with text=this is shit. The asterisk gave ****, the hyphen gave ----, the underscore gave ____ and the tilde gave ~~~~.

Other characters fail. fill_char=@, fill_char=Z and fill_char=1 each returned {"error":"Invalid User Replacement Characters"}. So did fill_char=ab.

The check does not wait for a match. text=clean text with fill_char=Z returned the same error. Text under three characters skips the check. text=ab with fill_char=Z returned {"result":"ab"}, but text=abc returned the error.

fill_text replaces the word with a fixed string. text=this is shit with fill_text=[bleep] returned {"result":"this is [bleep]"}. When we sent fill_text=[x] and fill_char=* together, fill_text won.

Formats and CORS

The service has json, xml, plain and containsprofanity paths. GET /service/plain?text=this is shit returned the text this is **** with no wrapper. The xml path wraps the result in a PurgoMalum element.

A request with Origin: https://example.com to /service/json returned content-type: application/json and access-control-allow-origin: *. A browser page can call it directly.

Response times in our probes ran from about 0.5 s to 2 s.

PurgoMalum, by the numbers

Rendered live from the atlas entry
AuthenticationNone required
HTTPSSupported
CORSEnabled
PricingFree
FormatsJSON, XML, Plain Text
profanity-filtercontent-moderationtext-validationobscenity-filter

Sources

Facts checked Nov 2026