Skip to content

Support 'allowElements' to allow custom tag names. - #402

Open
isoos wants to merge 1 commit into
google:masterfrom
isoos:custom-elements
Open

Support 'allowElements' to allow custom tag names.#402
isoos wants to merge 1 commit into
google:masterfrom
isoos:custom-elements

Conversation

@isoos

@isoos isoos commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

To enable supporting dartdoc's callout-box: dart-lang/pub-dev#9488

@sigurdm sigurdm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also allow a custom set of attributes now we are making things non-fixed?

Comment on lines +261 to +262
if (!_allowedElements.contains(tagName) &&
!(allowElements?.call(tagName) ?? false)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

de morgan's?

Suggested change
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs more explanation.

Are the elements allowed by this function allowed on top of the basic allowlist or exclusively these?

isoos added a commit to SanskaarUndale21/pub-dev that referenced this pull request Aug 7, 2026
jonasfj added a commit to dart-lang/pub-dev that referenced this pull request Aug 12, 2026
* 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>
@jonasfj

jonasfj commented Aug 12, 2026

Copy link
Copy Markdown
Member

Should we also allow a custom set of attributes now we are making things non-fixed?

Yes, if we are going down this route, then maybe the better choice is to accept a Policy object.

If you look at the code:
https://github.com/isoos/dart-neats/blob/e7ffc01a919ff2583ce9f3c35f4e2c2753f738a5/sanitize_html/lib/src/sane_html_validator.dart

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 package:html and just emulate the SAX parser as we traverse the DOM.

@jonasfj

jonasfj commented Aug 12, 2026

Copy link
Copy Markdown
Member

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 rel attributes with this pattern.

final Iterable<String>? Function(String)? addLinkRel;

SaneHtmlValidator({
required this.allowElements,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have string objections, to this.

@isoos

isoos commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Note: we should be able to easily allow @youtube marker support that has an output like this:
dart-lang/pub-dev#9490 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants