HTML, chmod, containers and cron

The reasoning behind four utilities: why an HTML editor needs an allowlist and a sandbox, why the capital S in a chmod string matters, what a docker run command cannot express in compose, and the two cron rules that catch everyone.

The HTML editor: an allowlist, not a filter

The editor produces HTML, and HTML that came from a text box is the oldest injection vector there is. Two different questions get confused here, so it is worth separating them. The first is "can this editor be tricked into emitting markup that runs code in the page that displays it?" The second is "is this a safe way to clean hostile HTML from an untrusted source?" This tool answers yes to the first and no to the second, and it says so on the panel rather than letting you assume otherwise.

The mechanism is an allowlist. A list of elements is permitted; every other element is unwrapped, so its text survives and its tag does not. A list of attributes is permitted per element; everything else is dropped, which is what makes every event handler impossible rather than merely blocked. Attribute names are lowercased before that test, so oNeRrOr and ONERROR are the same rejected name as onerror. Elements whose contents the HTML parser treats as raw text — script, style, textarea, iframe, title — are removed together with their contents, because unwrapping them would paste script source into the document as markup.

Link addresses get their own check. The value is entity-decoded and stripped of whitespace and control characters before its scheme is read, because that is what a browser does when it navigates: javascript:alert(1) and jav ascript:alert(1) both reach the same place. Only http, https, mailto, tel, ftp and relative addresses survive. A character reference the tool cannot resolve — : is the classic — causes the whole address to be refused rather than passed through, because a filter that understands less of the input than the browser does is a filter with a hole in it.

There is no image button, and img is not on the allowlist. An allowed image tag is an outbound request from wherever the document ends up, and an onerror surface. For an editor that produces prose, it is not worth either.

The defence that does not depend on any of this being perfect is the preview. It renders in an iframe with a sandbox attribute that grants nothing at all — no scripts, no forms, no same-origin access. If a payload ever survived the allowlist, the browser would still refuse to execute it there. That is a structural guarantee rather than a promise about our code.

The honest limitation: this is a string-based sanitiser, not a full HTML5 parser. Parser differentials — inputs where a browser builds a different tree than this tokeniser did — are the known weakness of every sanitiser built this way. For output you generate yourself in this editor that is fine. For cleaning HTML that arrived from a stranger, use a sanitiser running against a real parser, on a server you control.

Chmod: the capital letter is the whole message

A Unix file mode is twelve bits: three permission bits for each of owner, group and other, plus setuid, setgid and the sticky bit. The octal form is a direct reading of those bits, which is why 4 + 2 + 1 = 7 means read, write and execute.

The symbolic form that ls prints is where the special bits become interesting, because they do not get their own column. setuid replaces the owner execute character, setgid replaces the group execute character, and sticky replaces the other execute character. The letter is lower case when the underlying execute bit is also set and UPPER case when it is not.

So 4755 is rwsr-xr-x — a setuid program that can be executed — while 4644 is rwSr--r--, a setuid file that nobody can run. The capital S is the system telling you the bit is inert, and it is almost always a mistake rather than a choice. The same applies to 1644, which shows rw-r--r-T: a sticky bit on a regular file where the other-execute bit is clear, which on a modern kernel does precisely nothing.

Two more distinctions worth keeping straight. The nine-character string ls prints is not the argument chmod takes: chmod wants an assignment list such as u=rwx,g=rx,o=rx,u+s. And the same bits mean different things on a directory than on a file — execute means "may enter and traverse", setgid means "new files inherit this group", and the sticky bit means "anyone may create, only the owner may delete", which is the reason /tmp is 1777.

docker run to compose: parsing, and what does not convert

The converter splits the command with a quote-aware tokeniser written for this tool. Nothing is executed, evaluated, or handed to a shell. A command containing backticks, $(...) or a semicolon is read as literal characters, because the only thing this tool does with your input is read it.

Most flags map cleanly: ports, volumes, environment, labels, restart policy, capabilities, health checks, resource limits, DNS and extra hosts all have compose keys with the same meaning. Named volumes and user-defined networks need a top-level declaration as well as the service reference, so the output carries one — with external: true on networks, because a network docker run could join was one that already existed.

Some flags have no compose equivalent, and this is where converters usually fail quietly. --rm is not a service property: a compose service is not removed when it exits. -d describes how you invoked the CLI, not what the service is. --link is legacy, --mount needs a long form this tool does not write, --gpus depends on your driver. Each of these produces a named warning instead of vanishing. An unknown flag produces a warning too, and is treated as taking no value, because guessing that it took one would turn your image name into an argument and produce a confidently wrong file.

The output carries no top-level version: key. Compose v2 ignores it and current versions warn about it; it has been obsolete since the Compose Specification replaced the old file-format versions.

Quoting is not cosmetic. A port mapping written as 8080:80 without quotes is read as a number by YAML 1.1 parsers, and yes, no, on and off are booleans in the same dialect. Anything that could be misread is quoted; anything that reads cleanly as a string is left alone, so the file stays readable.

Cron: two rules that catch everyone

The first is the day rule. When both day-of-month and day-of-week are set to something other than *, cron runs the job when EITHER matches — not when both do. So 0 0 13 * 5 is "midnight on the 13th, and midnight every Friday", which is roughly five times a month, not "Friday the 13th", which is once or twice a year. The generator warns whenever both fields are restricted, and the sentence it writes says "or" rather than "and".

The second is the time zone. Cron uses the zone of the machine it runs on, which is frequently UTC on a server and almost never the zone of the person writing the crontab. The next-run preview therefore asks which zone to use rather than assuming yours. Times are computed as wall-clock times in that zone, so a daily job stays at the same local time across a daylight-saving change rather than drifting by an hour — and a local time that does not exist, in the hour skipped every spring, is left out of the list, because cron will not run then either.

The syntax itself is small: a star, a number, a range a-b, a list a,b,c, a step */n or a-b/n, and the month and day names JAN-DEC and SUN-SAT. Both 0 and 7 mean Sunday. A bare value with a step, such as 5/15 in the minute field, means "from 5 to the end of the range, every 15", which is minute 5, 20, 35 and 50.

Validation is strict on purpose. 60 * * * * is refused because minutes stop at 59; * * * * 8 is refused because the day field stops at 7; */0 * * * * is refused because a step of zero matches nothing. Six fields usually means a Quartz or systemd expression whose first field is seconds, so the error says that instead of only counting. A schedule that is syntactically valid but can never run — 0 0 30 2 * asks for the 30th of February — is accepted and flagged, because refusing it would be wrong and staying silent would be worse.

What happens to what you paste

  • Every conversion, hash, decode and diff runs in your browser tab. No input is uploaded, logged or stored on a server, because there is no server involved once the page has loaded.
  • Hashes come from the browser’s own Web Crypto implementation, and UUIDs from its cryptographically secure random generator. Neither involves a network call.
  • Nothing you type is written to local storage or a cookie. Reloading the page discards it; closing the tab discards it.
  • There is no analytics script, no advertising script and no third-party request of any kind. You can confirm all of this in your browser’s network panel — the page makes no requests after it loads.
  • That said: a JWT or an API key is a live credential. The safe habit is never to paste one into a web page you did not write, however trustworthy its claims — including this one.

Questions

Can I paste hostile HTML into the editor to clean it?

You can paste it, and nothing will execute: the paste handler sanitises the clipboard payload before anything is inserted, and the preview is a sandboxed iframe without allow-scripts. But do not use the result as a security control for untrusted input. This is a string-based allowlist for one editor’s own output, not a general-purpose XSS filter, and a browser’s parser can reach a different tree than a tokeniser like this one does. For untrusted content, sanitise on a server with a real HTML5 parser.

Why does the editor delete my images and tables?

Because they are not on the allowlist. Images are an outbound request and an onerror surface; tables are simply outside what this editor is for. Their text content survives, the tags do not. That is the allowlist working rather than a bug — but it does mean this is not the right tool for editing a complex existing document.

Why does chmod 4644 show a capital S?

Because setuid is set and the owner execute bit is not. The capital letter means the special bit has nothing to act on: it is a setuid file that cannot be executed. Lower case s means both bits are set. The same rule gives you S for setgid and T for the sticky bit, and getting it backwards is the single most common error in mode calculators.

Does the docker converter run my command?

No, and it cannot. The command is split by a tokeniser that understands quoting and nothing else. There is no eval, no shell, no child process — the page has no way to start one. Everything you paste stays in the tab.

Why does the converter warn instead of just dropping a flag it cannot handle?

Because a compose file that silently does less than the command it came from is worse than no output at all: you find out in production. Every flag that is not represented is named, with what to do about it. If you see no warnings, nothing was lost.

Why are the next cron runs different from what my server does?

Almost always the time zone. Cron uses the zone of the machine running it; the preview uses the zone you picked in the dropdown. Set it to your server’s zone — often UTC — and they should agree. The other candidate is the day rule: if both day-of-month and day-of-week are set, cron runs on either, which is more often than most people intend.

Limitations

  • The HTML sanitiser is an allowlist for this editor’s own output, implemented as a string tokeniser rather than a full HTML5 parser. It is not a general-purpose XSS filter and must not be used as one.
  • The editor has no images, tables, colours or font controls, and no way to add them: everything outside the allowlist is removed.
  • The editing surface uses document.execCommand, which is deprecated and has no implemented replacement. Browsers still support it; behaviour differs slightly between them, especially around list nesting.
  • The chmod calculator covers the twelve permission bits only. Access control lists, extended attributes, SELinux contexts, umask arithmetic and Windows ACLs are all out of scope.
  • The docker converter translates a single docker run command into a single service. It does not read Dockerfiles, does not resolve images, and cannot express --mount, --gpus, --device, --link or --volumes-from; each of those produces a named warning instead.
  • Flags the converter does not recognise are treated as taking no value. If such a flag did take one, the value will be misread — the warning says so, and removing the flag is the fix.
  • The cron generator reads the five-field crontab format only. Six-field Quartz and systemd expressions, @reboot, and the non-standard L, W, # and ? operators are refused rather than guessed at.
  • Next-run times come from the browser’s own time-zone database through Intl. They are a preview, not a guarantee: the machine running cron may have a different zone, a different database version, or a daemon that was not running when the time came round.

Last reviewed 2026-09-14.