Support 'allowElements' to allow custom tag names. - #402
Conversation
sigurdm
left a comment
There was a problem hiding this comment.
Should we also allow a custom set of attributes now we are making things non-fixed?
| if (!_allowedElements.contains(tagName) && | ||
| !(allowElements?.call(tagName) ?? false)) { |
There was a problem hiding this comment.
de morgan's?
| if (!_allowedElements.contains(tagName) && | |
| !(allowElements?.call(tagName) ?? false)) { | |
| if (!(_allowedElements.contains(tagName) || | |
| (allowElements?.call(tagName) ?? false))) { |
| /// guard against XSS as this sanitizer also attempts to prevent the sanitized | ||
| /// HTML from interfering with the page it is injected into. | ||
| /// | ||
| /// The optional [allowElements] callback will be called with uppercase tag names. |
There was a problem hiding this comment.
This needs more explanation.
Are the elements allowed by this function allowed on top of the basic allowlist or exclusively these?
* Fix inconsistent hover effects on dark-background elements (#8886) Several elements sit on permanently-dark surfaces (site header, footer, home banner, nav popup) but inherited the global link hover rule that applies brightness(80%) in light mode, dimming already-bright text and reducing readability. Override those specific contexts to use brightness(120%) so hovering always increases visibility regardless of the active color theme. Also replace the opacity: 0.8 dimming on .nav-button hover with a brightness filter to stay consistent with the rest of the header. Finally, suppress the brightness filter on anchor elements that directly wrap a .pub-monochrome-icon, since those icons already have their own opacity-based hover feedback and applying both mechanisms produced an unintended compounded visual effect. Fixes #8886 * Refactor hover fix to use semantic .pub-dark-bg CSS class Replaces per-element light-theme overrides scattered across _site_header.scss, _footer.scss, and _home.scss with a single .pub-dark-bg rule in _base.scss. Applies the class to site-header, site-footer, and _banner-bg in the Dart templates. * x * Tag 20260507t120800-all (#9381) * Migrating task model and backend to use typed_sql. (#9126) * Fix migration in prod env. (#9385) * Tag 20260512t124000-all (#9386) * Complete the package:web migration of pkg/web_app (#9387) * Revert "Migrating task model and backend to use typed_sql. (#9126)" (#9393) This reverts commit ad3dd7f. * Log more on package sync error. (#9394) * Load/save Postgres database state into local state file. (#9389) * Test updates: await on async queue, check for no dart:html use. (#9390) * Revert "Load/save Postgres database state into local state file. (#9389)" (#9397) This reverts commit 2c7034c. * Bump actions/cache from 5.0.4 to 5.0.5 in the github-actions group (#9365) Bumps the github-actions group with 1 update: [actions/cache](https://github.com/actions/cache). Updates `actions/cache` from 5.0.4 to 5.0.5 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](actions/cache@6682284...27d5ce7) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 5.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Revert "Make advisoriesUpdated non-nullable in API. (#9376)" (#9396) This reverts commit 1fb03b6. * Upgrade analysis SDKs. (#9400) * Tag 20260519t092000-all (#9401) * Upgrade the runtime SDK + CI. (#9404) * signature_verifier in Dart+webcrypto (#9408) * Upgrade dependencies: appengine, gcloud, googleapis, typed_sql (#9409) * pub_integration test sharding on CI (#9411) * Split TokenIndex's weighted doc list + use quantized values for weights. (#9403) * Simpler CachedValue logic + more tests. (#9402) * Revert "signature_verifier in Dart+webcrypto (#9408)" (#9413) This reverts commit 2b9a4b8. * Tag 20260528t110400-all (#9414) * CI-only test for building Dockerfile.app (#9415) * Migrating task model and backend to use typed_sql. (#9410) * Runtime version bump for task SQL migration. (#9416) * Tag 20260601t084200-all (#9417) * Reapply "Load/save Postgres database state into local state file. (#9389) (#9399) * Bump actions/stale from 10.2.0 to 10.3.0 in the github-actions group (#9421) * Upgrade analysis SDKs (#9420) * Fix task scan in backfillTrackingState. (#9424) * Preserve Stacktrace when extracting inner transaction. (#9426) * Fix catch-all in task scheduling. (#9427) * Fix DB connection string parameter trimming. (#9429) * Fix transaction wrapper in trackPackage. (#9425) * TokenIndex with a single binary data structure for postings list (#9419) * Tag 20260611t083300-all (#9435) * Replace material design select widget to plain HTML <select>. (#9431) * Refactor/rename task access methods. (#9428) * Migrate textfield + textarea from material component to plain HTML + updated style rules. (#9436) * Minimal search optimization + removal of unused code. (#9437) * Upgrade analysis SDKs (#9439) * Run-wrapper for services. (#9423) * Migrate Material datatable to plain HTML (#9442) * Upgrade dartdoc to 9.0.5 (#9443) * Scan tar archive against maxUncompressedSize, not maxArchiveSize (#9440) maxTotalLengthBytes counts uncompressed bytes. In TarArchive.scan * Migrate material checkbox to plain HTML. (#9438) * Upgrade typed_sql + postgres. (#9444) * Tag 20260618t095000-all (#9445) * Merge description + readme token index. (#9446) * Migrate the material floating action button to plain HTML. (#9447) * Migrate button component to plain HTML (#9448) * Upgrade analysis Flutter SDK (#9449) * Upgrade pana (#9453) * Tag 20260625t134000-all (#9454) * Use typed_sql's snake_case naming override for SQL with Dart field names. (#9451) * Test to verify what happens when email changes. (#9459) * Improve name search by changing Set.contains into counting. (#9458) * Use int-based counter in search name matching. (#9463) * Bump actions/cache from 5.0.5 to 6.1.0 in the github-actions group (#9466) * Name index update: ngram-based posting lists (#9465) * Upgrade analysis SDK, pana, dartdoc (#9468) * Migrate the dialog component to plain HTML. (#9450) * Remove material web components. (#9467) * Quantized score collection (#9471) * Tag 20260702t101200-all (#9469) * Upgrade Flutter SDK + dependencies (#9473) * Remove (unused) UserInfo. (#9475) * Reference to primaryDatabase is always non-null. (#9476) * Fix task queries + add indexes. (#9474) * More robust analysis download. (#9478) * Upgrade dartdoc to 9.0.8 (#9480) * Fix mobile metadata back-button getting stuck (regression of #7428) (#9481) * UserSession migration to SQL (dual use in Postgres and Datastore) (#9482) * Tag 20260714t095300-all (#9483) * Upgrade dependencies + analysis Flutter SDK (#9491) * Fix: update dartdoc resources (styles, JS, icons). (#9492) * Copy google/dart-neats#402 into pkg/sanitize_html and exempt callout-box (#9494) * Upgrade pana to 0.23.15 and run only the patched HTML sanitization. (#9495) * register static files for test scope (#9496) * Tag 20260730t085500-all (#9497) * Further cleanup after removing material styles. (#9493) * Allow regional Google Analytics and GTM domains in CSP connect-src (#9500) * Rename follow-up: start the cleanup of Package.automatedPublishing field. (#9484) * Bump actions/stale from 10.3.0 to 11.0.0 in the github-actions group (#9499) * Switch UserSession logic to read only from Postgres (but keep writing to Datastore) (#9506) * Analysis task SQL post-migration Datastore cleanup (PackageState dele… (#9504) * Configure CSP report-to directive, Reporting-Endpoints header, and reporting endpoint (#9505) * Configure CSP report-to directive and reporting endpoint * Add legacy report-uri fallback and noise filtering to CSP reporting endpoint * Use lightweight TestProfile for CSP reporting tests * Upgrade Flutter analysis SDK + pana (#9501) * Tag 20260805t095600-all (#9508) * Fix backfill comparison issue (#9509) * Allow Google Feedback, gstatic, and google.com in CSP (#9512) * Upgrade pana (#9513) * Tag 20260806t120500-all (#9514) * Updated golden files. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Istvan Soos <istvan.soos@gmail.com> Co-authored-by: Sigurd Meldgaard <sigurdm@google.com> Co-authored-by: István Soós <isoos@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Xavier H. <xavier.hainaux@gmail.com> Co-authored-by: Jota López <102370043+code3743@users.noreply.github.com> Co-authored-by: Jonas Finnemann Jensen <jonasfj@google.com> Co-authored-by: Alexander Thomas <athom@google.com>
Yes, if we are going down this route, then maybe the better choice is to accept a If you look at the code: We could make a policy object that is something like: /// Rules for sanitizing HTML in a high-performance SAX-style pass.
///
/// A [SanitizerRules] implementation is stateful and must be used for a
/// single sanitization pass. This allows the implementation to track
/// element nesting and attribute state to make complex decisions.
///
/// The parser using these rules should prioritize yielding original
/// substrings from the input HTML whenever actions return 'Keep'.
abstract base class SanitizerRules {
/// Called when an element start tag `<tagName` is encountered.
///
/// If this returns [RemoveElementAction], the tag and all its
/// children are stripped from the output.
///
/// If this returns [UnwrapElementAction], the tag itself is stripped,
/// but its children are processed and retained.
ElementAction beginOpenElement(String tagName);
/// Called for each `name="value"` pair on a kept element.
///
/// [value] is the decoded HTML entity value.
AttributeAction attribute(String tagName, String name, String value);
/// Called when the open tag is finished `>`.
///
/// Return an [Iterable] of `(name, value)` pairs to inject additional
/// attributes into the tag.
///
/// [isVoid] is true if the element is a void element (e.g. <img>, <br>).
/// [isSelfClosing] is true if the tag used the self-closing syntax `<tag/>`.
Iterable<(String name, String value)>? endOpenElement(
String tagName, {
required bool isVoid,
required bool isSelfClosing,
});
/// Called when an element's closing tag is encountered `</tagName>`.
///
/// For void elements, the parser must call this immediately after
/// [endOpenElement] to maintain stack consistency for the rules.
void closeElement(
String tagName, {
required bool isVoid,
required bool isSelfClosing,
});
/// Called for text content between tags.
TextAction text(String text);
/// Called for HTML comments `<!-- content -->`.
///
/// Return [KeepTextAction] to keep the comment (as-is),
/// [RemoveTextAction] to strip it, or [RewriteTextAction] to
/// replace the content of the comment.
TextAction comment(String content);
const SanitizerRules();
}
/// Action to take for text content.
sealed class TextAction {
const TextAction._();
/// Keep the text content as-is.
factory TextAction.keep() => KeepTextAction();
/// Remove the text content.
factory TextAction.remove() => RemoveTextAction();
/// Rewrite the text content to [newValue].
factory TextAction.rewrite(String newValue) = RewriteTextAction;
}
/// Keep the text content as-is.
final class KeepTextAction extends TextAction {
static const KeepTextAction _instance = KeepTextAction._();
const KeepTextAction._() : super._();
factory KeepTextAction() => _instance;
}
/// Remove the text content.
final class RemoveTextAction extends TextAction {
static const RemoveTextAction _instance = RemoveTextAction._();
const RemoveTextAction._() : super._();
factory RemoveTextAction() => _instance;
}
/// Rewrite the text content.
final class RewriteTextAction extends TextAction {
final String newValue;
const RewriteTextAction(this.newValue) : super._();
}
/// Action to take for an HTML element.
sealed class ElementAction {
const ElementAction._();
/// Keep the element and its attributes.
factory ElementAction.keep() => KeepElementAction();
/// Remove the element and all its children.
factory ElementAction.remove() => RemoveElementAction();
/// Remove the tag but keep and sanitize its children.
factory ElementAction.unwrap() => UnwrapElementAction();
/// Rename the element to [newTagName].
///
/// The original attributes are preserved and passed to [SanitizerRules.attribute].
factory ElementAction.rename(String newTagName) = RenameElementAction;
/// Replace the element and its children with [replacementHtml].
///
/// The [replacementHtml] is inserted as-is and is **NOT sanitized**.
factory ElementAction.replace(String replacementHtml) = ReplaceElementAction;
}
/// Keep the element and its attributes.
final class KeepElementAction extends ElementAction {
static const KeepElementAction _instance = KeepElementAction._();
const KeepElementAction._() : super._();
factory KeepElementAction() => _instance;
}
/// Remove the element and all its children.
final class RemoveElementAction extends ElementAction {
static const RemoveElementAction _instance = RemoveElementAction._();
const RemoveElementAction._() : super._();
factory RemoveElementAction() => _instance;
}
/// Remove the tag but keep and sanitize its children.
final class UnwrapElementAction extends ElementAction {
static const UnwrapElementAction _instance = UnwrapElementAction._();
const UnwrapElementAction._() : super._();
factory UnwrapElementAction() => _instance;
}
/// Rename the element to [newTagName].
final class RenameElementAction extends ElementAction {
final String newTagName;
const RenameElementAction(this.newTagName) : super._();
}
/// Replace the element and its children with [replacementHtml].
final class ReplaceElementAction extends ElementAction {
final String replacementHtml;
const ReplaceElementAction(this.replacementHtml) : super._();
}
/// Action to take for an attribute.
sealed class AttributeAction {
const AttributeAction._();
/// Keep the attribute as-is.
factory AttributeAction.keep() => KeepAttributeAction();
/// Remove the attribute.
factory AttributeAction.remove() => RemoveAttributeAction();
/// Rewrite the attribute value to [newValue].
factory AttributeAction.rewrite(String newValue) = RewriteAttributeAction;
}
/// Keep the attribute as-is.
final class KeepAttributeAction extends AttributeAction {
static const KeepAttributeAction _instance = KeepAttributeAction._();
const KeepAttributeAction._() : super._();
factory KeepAttributeAction() => _instance;
}
/// Remove the attribute.
final class RemoveAttributeAction extends AttributeAction {
static const RemoveAttributeAction _instance = RemoveAttributeAction._();
const RemoveAttributeAction._() : super._();
factory RemoveAttributeAction() => _instance;
}
/// Rewrite the attribute value.
final class RewriteAttributeAction extends AttributeAction {
final String newValue;
const RewriteAttributeAction(this.newValue) : super._();
}I drafted this a while ago, but never had time to finish it. The idea was to power it with SAX style parser, but there is no reason we could use it with |
|
Another option would be to just accept a Map like we have here: final _commonAttributeValidator = {
for (final a in _alwaysAllowedAttributes)
a: _alwaysAllowed,
};
final policy =
<String, Map<String, bool Function(String)>>{
for (final e in _allowedElements)
e: _commonAttributeValidator,
'A': {
'href': _validLink,
},
'IMG': {
'src': _validUrl,
'longdesc': _validUrl,
},
'SOURCE': {
'srcset': _validSrcset,
},
'DIV': {
'itemscope': _alwaysAllowed,
'itemtype': _alwaysAllowed,
},
'BLOCKQUOTE': _citeAttributeValidator,
'DEL': _citeAttributeValidator,
'INS': _citeAttributeValidator,
'Q': _citeAttributeValidator,
};We just can't rewrite attributes, or add |
| final Iterable<String>? Function(String)? addLinkRel; | ||
|
|
||
| SaneHtmlValidator({ | ||
| required this.allowElements, |
There was a problem hiding this comment.
I don't have string objections, to this.
|
Note: we should be able to easily allow |
To enable supporting dartdoc's callout-box: dart-lang/pub-dev#9488