Regex Docker image reference (name:tag@digest)
An image reference is written [registry[:port]/]path[:tag][@digest]. Path components are lowercase letters and digits, separated by a period, one or two underscores or several hyphens; the registry is a DNS name (or a bracketed IPv6 address) with an optional port; the tag is 1 to 128 characters among letters, digits, underscore, period and hyphen, not starting with a period or hyphen; the digest is algorithm:hexadecimal of at least 32 digits. These are the rules of Docker/distribution's reference package. The three real attempts I read depart from them: Aspire's parser accepts uppercase, '<tag>' and any digest, docker-parse-image's accepts almost any string, and the JavaScript port of an older Docker pattern did not finish in over 100 seconds on 40 'a' followed by '!'. Here's the official pattern, converted to JavaScript.
/^(?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\[[a-fA-F0-9:]+\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*(?::[\w][\w.-]{0,127})?(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*:[0-9a-fA-F]{32,})?$/Comparison of existing patterns
| Source | pattern | Score on the corpus | ReDoS |
|---|---|---|---|
| Aspire (.NET, ContainerReferenceParser) — accepte les majuscules (« Ubuntu »), un tag commençant par un tiret, « <tag> », un tag de plus de 128 caractères, « a___b » et n'importe quel digest ; groupes nommés convertis tels quels | ^((?<registry>((localhost|\[(?:[a-fA-F0-9:]+)\]|([\w-]+(\.[\w-]+)+))(:\d+)?)|([\w-]+:\d+))\/)?(?<image>[\w-./<>]+)(:(?<tag>[\w-.<>]+))?(@(?<digest>.+))?$ | 16 / 27 | safe |
| docker-parse-image (npm) — conçu pour découper, pas valider : accepte espaces, majuscules, « http://registry/app » et « ubuntu:latest:extra » | ^(?:([^\/]+)\/)?(?:([^\/]+)\/)?([^@:\/]+)(?:[@:](.+))?$ | 14 / 27 | safe |
| @jkcfg/kubernetes (npm, image-reference.js) — port JavaScript d'un ancien motif de Docker : séparateur « [-]* » imbriqué sous des quantificateurs, backtracking catastrophique (40 « a » suivis de « ! » : plus de 100 s sans réponse) ; pas d'IPv6 | ^((?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?\/)?[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:\/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\w][\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9a-fA-F]{32,}))?$ | 26 / 27 | vulnerable (g/ag0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g0g…) |
Implementation
const re = new RegExp("^(?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\\[[a-fA-F0-9:]+\\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*(?::[\\w][\\w.-]{0,127})?(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*:[0-9a-fA-F]{32,})?$", "");
re.test(input);Limits
- Valide seulement la FORME : ne prouve pas que l'image ou le registre existent, ni que le digest correspond à un contenu. Le digest n'est pas vérifié : seul « algorithme:32 hexadécimaux ou plus » est contrôlé (sha256 attend 64 chiffres).
- Le code officiel de Docker limite aussi le nom (registre compris) à 255 caractères, hors de la regex : ce motif ne vérifie pas cette longueur. Il accepte « ubuntu » seul, sans registre ni tag, et ne normalise pas (docker.io/library/, tag latest).
- Un nom de la forme « a.b/c » est lu comme registre « a.b » : la règle « domaine si un point ou un port » appartient au code, pas au motif. Les registres internationalisés (IDN) et les zones IPv6 sont rejetés, comme dans le motif officiel.