What policy do you set for template.HTML, template.JS and template.URL across a team's codebase?
answer
- the only escaping off-switch
- default deny needs a sanctioned path
- concentrate conversions in one package
- a CI grep beats a wiki page
- each exception has an owner and a test
basics
~20 sTreat the conversions as the only escaping off-switch and gate them like unsafe code: default deny, one small reviewed package holding every sanctioned conversion, a mechanical check that fails the build elsewhere, and a named owner for each exception.
solid answer
~50 sThese types are the only way to turn `html/template`'s escaping off, so the policy question is who may do that and where. I default to deny, but a bare ban fails — engineers meet a real formatting need and route around it with string concatenation or client-side markup, which is worse. So the policy has three parts. One reviewed package owns every conversion, exposed as named constructors that say what justifies them: sanitised output, developer-authored constants, composed template output. A mechanical check — a CI grep over the conversion literals — fails any new site outside that package. And each sanctioned site carries a rendering test with hostile input. Exceptions are time-boxed and owned by a person. The security reviewer holds the gate; overruling it is a recorded decision, not an argument in a thread.
go deeper
Know that turning escaping off is somebody else's decision to sign off. If a value seems to need template.HTML, raise it rather than converting it yourself.
Be able to state the rule your team follows and where the sanctioned conversion helpers live, so the reflex is to call a named constructor rather than to cast in a handler.
Argue the enforcement mechanism: a CI check on conversions outside one package beats a review convention, and every sanctioned site carries a rendering test with hostile input.
Own the gate end to end — default deny, one allowlisted package, a funded sanitising path so the ban does not push people into worse workarounds, named ownership and expiry per exception, and a stated position on what the strictness costs.
## Why this is a policy question at all Most `html/template` questions have a right answer. This one does not, because the constraint is organisational: `template.HTML`, `template.JS`, `template.JSStr`, `template.URL`, `template.HTMLAttr` and `template.CSS` are the only mechanism that disables escaping, the conversion is a single token, and it appears in exactly the situation where someone is under pressure to make a page render correctly. You cannot inspect your way out of that with attentive reviewers; you have to change where the decision is allowed to be made. ## Default deny, with a sanctioned path Start from: no conversion of a value derived from a request, a stored request, a header, a filename, or a third-party response. That is the entire class that matters. The failure mode of a bare prohibition is worth stating plainly, because it is the tradeoff a lead is being asked about. A team told "never do this", with no supported way to render user-authored formatting, does not stop shipping the feature. They concatenate strings and write them with a different writer, or they ship the raw text to the browser and set it as markup from JavaScript, or they quietly render with `text/template`. Each of those moves the same defect somewhere with less review attention. So the policy must come with the supported alternative: a sanitising path the team can actually use, and a template pattern for the common cases (paragraph structure, links, emphasis) that removes the motivation entirely. ## Concentrate, then automate The structural move is to make the conversions rare and locatable. One small package — call it whatever your codebase calls such things — exports named constructors instead of raw conversions: one that takes text through the sanitiser and returns `template.HTML`, one that takes a compile-time constant of developer-authored markup, one that composes the output of another template execution. Call sites read as a claim (`SanitizedHTML(body)`) rather than as a cast, and the sanitiser can never drift away from the conversion because they are one function. Then make it enforceable. A wiki page is not a control; a CI step is. The conversions are literal text, so a grep for `template.HTML(`, `template.JS(`, `template.URL(`, `template.HTMLAttr(` outside the allowlisted package, failing the build, costs almost nothing and cannot be forgotten under deadline. Put that package under review ownership by whoever holds the security gate, so any change to the allowlist is seen by design rather than by luck. ## Who owns an exception Every genuine exception gets a person, not a team, and an expiry. The reviewer who signs off owns the claim that this specific value cannot be attacker-influenced — and that claim decays: a field that was admin-only becomes self-service two quarters later, an internal feed acquires a partner integration. The recorded owner is what makes a periodic re-examination possible instead of archaeological. The "it comes from an internal admin" argument deserves a stock response, because it is the most common one. The policy is about the call site and its lifetime, not about today's data source; internal accounts get compromised, and stored markup from an internal user is served to every external one. ## What each exception must carry A rendering test with hostile input, asserting the bytes actually rendered for that field in every position it occupies on the page. If the exception is a sanitiser, the test is a small corpus of hostile strings that must survive a sanitiser upgrade — which also settles who pays when the dependency changes behaviour: the owner of the exception, with a regression suite that tells them immediately. ## How to argue the tradeoff in the room Be explicit that strictness has a price and say what you are buying. The gate slows down a handful of changes a quarter and, in exchange, makes the set of places where escaping can be off small enough to enumerate and re-review in an hour. Be equally explicit about what you will not trade: you will fund the sanctioned path, because a policy whose only effect is to push the same behaviour into less-reviewed code has made the system worse while looking stricter. That, rather than the ban itself, is the judgment being tested.
- How do you keep the rule enforceable rather than aspirational?Make it mechanical. The conversions are literal text, so a CI check that fails on any occurrence outside the allowlisted package enforces it without depending on reviewer attention. Put that package under the security reviewer's review ownership so changes to the allowlist are seen by design, and keep the list short enough to re-read in one sitting.
- A developer argues the value is safe because only internal admins can set it. How do you respond?The policy governs the call site, not today's data source. Internal accounts get compromised, admin-only fields become self-service, and stored markup written by one internal user is served to every external one. If the exception is granted anyway, it is granted to a named owner with an expiry and a hostile-input test, not as a general dispensation.
- What breaks if you ban the conversions outright with no supported alternative?The requirement does not go away, so it reappears somewhere with less scrutiny: markup assembled by string concatenation, raw text handed to the client and inserted as HTML there, or a switch to text/template. You have moved the same defect out of the place your reviewers look at, which is a net loss.
- Who pays when a sanitiser upgrade changes what it strips?The owner recorded on the exception. That is the point of naming one: the upgrade is their regression run against the hostile-input corpus, and their call whether the behaviour change is acceptable. Without an owner the upgrade lands as a silent rendering change nobody is accountable for.
saying these in an interview costs you the question
- Relies on a wiki rule instead of an automated check
- Bans the conversions with no sanctioned alternative
- Trusts values because an internal account supplied them
- Allows conversions anywhere provided review is careful
- Grants exceptions to a team rather than a person