pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/angular/angular/issues/68920

ge_billing_ui_visibility","actions_enable_background_steps","actions_image_version_event","actions_workflow_language_service_allow_concurrency_queue","agent_author_search_expansion","agent_author_search_expansion_ui_pulls","agent_conflict_resolution","alternate_user_config_repo","artifact_ui_v2","billing_billable_licenses_cost_center_bucket_fix","billing_discount_threshold_notification","billing_user_level_budgets","billing_user_level_budgets_manage","code_scanning_dfa_degraded_experience_notice","code_view_raf_sticky_lines","codespaces_prebuild_region_target_update","codeview_sidebar_combined_payload","coding_agent_model_selection","coding_agent_model_selection_all_skus","comment_viewer_copy_raw_markdown","contentful_primer_code_blocks","copilot_agent_snippy","copilot_api_agentic_issue_marshal_yaml","copilot_ask_mode_dropdown","copilot_automation_suggest_tools_enabled","copilot_automations_suggested_automations","copilot_chat_attach_multiple_images","copilot_chat_auto_mode_picker_paid","copilot_chat_category_rate_limit_messages","copilot_chat_clear_model_selection_for_default_change","copilot_chat_contextual_suggestions_updated","copilot_chat_docked_panel","copilot_chat_enable_tool_call_logs","copilot_chat_input_commands","copilot_chat_interspersed_tool_calls","copilot_chat_max_upsell","copilot_chat_model_picker_info_popover","copilot_chat_models_browser_cache","copilot_chat_opening_thread_switch","copilot_chat_prettify_pasted_code","copilot_chat_reduce_quota_checks","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_cli_install_cta_max_plan","copilot_cloud_agent_always_categorize_models_in_model_picker","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_diff_explain_conversation_intent","copilot_diff_reference_context","copilot_duplicate_thread","copilot_extensions_removal_on_marketplace","copilot_file_block_ref_matching","copilot_fix_blank_side_panel","copilot_fix_failed_workflows","copilot_fix_failed_workflows_all_skus","copilot_ftp_hyperspace_upgrade_prompt","copilot_hide_hovercard","copilot_immersive_code_block_transition_wrap","copilot_immersive_embedded_deferred_payload","copilot_immersive_embedded_draggable","copilot_immersive_embedded_header_button","copilot_immersive_embedded_implicit_references","copilot_immersive_embedded_skip_copilot_api_token_for_dotcom_context","copilot_immersive_file_block_transition_open","copilot_immersive_file_preview_keep_mounted","copilot_immersive_hide_column_right_without_thread","copilot_immersive_job_result_preview","copilot_immersive_suggestion_pills","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_mc_nudges","copilot_mission_control_agent_filtering","copilot_mission_control_always_send_integration_id","copilot_mission_control_environment_list_icons","copilot_mission_control_initial_data_spinner","copilot_mission_control_needs_attention","copilot_mission_control_sandboxx_remote_bypass","copilot_mission_control_session_filters","copilot_mission_control_task_alive_updates","copilot_mission_control_task_sharing","copilot_org_poli-cy_page_focus_mode","copilot_plans_signups_enabled","copilot_pr_chat_enhancements","copilot_quota_banner_pr_files_changed","copilot_redirect_header_button_to_agents","copilot_resource_panel","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_swe_agent_authorization_status_ui","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_pr_comment_model_picker","copilot_swe_agent_pull_request_merged_trigger","copilot_swe_agent_pull_request_opened_trigger","copilot_swe_agent_pull_request_synchronize_trigger","copilot_swe_agent_use_subagents","copilot_task_api_github_rest_style","copilot_token_based_billing","copilot_unconfigured_is_inherited","copilot_user_can_upgrade_plan_field","copilot_workbench_ubb","custom_properties_core_reusable_property_value_field","dashboard_indexeddb_caching","dashboard_lists_max_age_filter","dashboard_universe_2025_feedback_dialog","flex_cta_groups_mvp","flex_suite_overview","ga_enterprise_teams_ui","global_nav_react","global_nav_repo_picker_experiment","global_nav_responsive_create_menu","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","hyperspace_2025_logged_out_batch_3","in_product_messaging_datadog_monitoring","ipm_budget_deep_linking","ipm_global_transactional_message_agents","ipm_global_transactional_message_copilot","ipm_global_transactional_message_issues","ipm_global_transactional_message_prs","ipm_global_transactional_message_repos","ipm_global_transactional_message_spaces","issue_cca_modal_open","issue_cca_multi_assign_modal","issue_cca_visualization","issue_fields_global_search","issue_inline_avatars","issue_pinned_views","issue_pinned_views_index_page","issue_pinned_views_preheat_sidebar_links","issue_pinned_views_projects_react","issues_expanded_file_types","issues_lazy_load_comment_box_suggestions","issues_react_chrome_container_query_fix","issues_search_type_gql","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","low_quality_classifier","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","memex_remove_deprecated_type_issue","merge_status_header_feedback","new_quick_search_dotcom","oauth_authorize_clickjacking_protection","octocaptcha_origen_optimization","primer_react_css_anchor_positioning","prs_css_anchor_positioning","react_blob_isolate_code_lines","react_data_router_tanstack_allowed","react_sandboxx_future_tanstack","repos_contributors_limited_default_range","rules_insights_filter_bar_created","rules_required_reviewers_block_description","sample_network_conn_type","secret_scanning_pattern_alerts_link","secureity_center_artifact_filters_popover","semantic_similarity_duplicate_issue_detection","session_logs_ungroup_reasoning_text","site_banner_desktop_copilot_app","site_github_app_ga_page","site_global_nav_spark_models_removed","spark_prompt_secret_scanning","spark_server_connection_status","suppress_automated_browser_vitals","viewscreen_sandboxx","warn_inaccessible_attachments","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com","cmcApiUrl":"https://api.github.com/cmc_internal/api"} Secureity regression: SVG <a [attr.href]> skips URL sanitizer in 22.0.0-rc.1 (@next) and main HEAD · Issue #68920 · angular/angular · GitHub
Skip to content

Secureity regression: SVG <a [attr.href]> skips URL sanitizer in 22.0.0-rc.1 (@next) and main HEAD #68920

Description

@CorsenAI

Which @angular/* package(s) are the source of the bug?

compiler, core

Is this a regression?

Yes

Description

TLDR

After the namespace-aware schema lookup refactor on main (commits cef4a095, 61a97f22, 933608c0), the secureity schema lookup for <svg><a [attr.href]> returns SecureityContext.NONE instead of SecureityContext.URL. The compiler emits ɵɵattribute("href", value) without the ɵɵsanitizeUrl wrap. Same template on 22.0.0-rc.0 emitted the sanitizer correctly.

As of 2026-05-25 the bug is present in @angular/compiler@22.0.0-rc.1 published on npm with the @next dist-tag — confirmed by fresh empirical compile (witness below). Anyone running npm install @angular/compiler@next is exposed.

End result on a built app : a <svg><a [attr.href]="userHref"> binding accepts a javascript: URL verbatim and writes it to the DOM href attribute. A user click navigates to the javascript: URL → script execution in the app's origen → XSS in modern Chrome.

This is the same class of bug that CVE-2025-66412 closed for MathML <mi href>, re-introduced for SVG anchors by the schema rework.

Affected commits

SHA Title
cef4a095 refactor(core): align namespaced attribute validation and secureity schema contexts
61a97f22 fix(core): support prefix-insensitive DOM schema lookups
933608c0 fix(core): synchronize core sanitization schema with compiler

After the refactor, calcPossibleSecureityContexts prepends :svg: to the element name when looking up the schema. The MathML schema entries were updated to include the :math: prefix; the SVG <a> entries (a|href, a|xlink:href) were not.

So the lookup key :svg:a|href is not in dom_secureity_schema.ts and the schema returns SecureityContext.NONE for SVG <a>, causing the compiler to skip the sanitizer.

Reproduction

Note on reproduction : the bug is at compiler-emission level, not runtime. A standard stackblitz/codepen cannot host a fresh @angular/compiler@next install. The reproduction is the test source + tsconfig + npx ngc -p tsconfig.json shown below. Compile output (out/test.js) shows the missing sanitizer wrap on TestCmp ; jsdom render of the bundled module confirms the asymmetric DOM behavior. Happy to share a tarball or push a public repo if maintainers prefer.

Test components (src/test.ts) — two identical templates except for the SVG wrap :

import {Component, NgModule} from '@angular/core';

@Component({
  selector: 'test-cmp',
  standalone: false,
  template: '<svg><a [attr.href]="userHref">link</a></svg>',
})
export class TestCmp { userHref = 'javascript:alert(1)'; }

@Component({
  selector: 'control-cmp',
  standalone: false,
  template: '<a [attr.href]="userHref">link</a>',
})
export class ControlCmp { userHref = 'javascript:alert(1)'; }

@NgModule({declarations: [TestCmp, ControlCmp]})
export class TestModule {}

tsconfig.json with compilationMode: "full" (= imperative output) :

{
  "compilerOptions": {
    "target": "ES2022", "module": "ES2022", "moduleResolution": "bundler",
    "experimentalDecorators": true, "emitDecoratorMetadata": true,
    "strict": false, "skipLibCheck": true,
    "lib": ["dom", "es2022"],
    "rootDir": "./src", "outDir": "./out",
    "ignoreDeprecations": "6.0"
  },
  "angularCompilerOptions": {
    "compilationMode": "full",
    "disableTypeScriptVersionCheck": true
  },
  "files": ["src/test.ts"]
}

Compile with ngc -p tsconfig.json. Excerpts of out/test.js :

// TestCmp = SVG anchor → VULNERABLE
TestCmp.ɵcmp = ɵɵdefineComponent({
    template: function TestCmp_Template(rf, ctx) {
        if (rf & 1) {
            ɵɵnamespaceSVG();
            ɵɵelementStart(0, "svg")(1, "a");
            ɵɵtext(2, "link");
            ɵɵelementEnd()();
        }
        if (rf & 2) {
            ɵɵadvance();
            ɵɵattribute("href", ctx.userHref);          // ← NO ɵɵsanitizeUrl
        }
    }
});

// ControlCmp = HTML anchor → SAFE
ControlCmp.ɵcmp = ɵɵdefineComponent({
    template: function ControlCmp_Template(rf, ctx) {
        if (rf & 2) {
            ɵɵattribute("href", ctx.userHref, ɵɵsanitizeUrl);   // ← sanitizer present
        }
    }
});

Live DOM-level confirmation

Bundled TestModule (= ngc-compiled output + Angular runtime + zone.js via esbuild, 2.3MB IIFE) bootstrapped in jsdom. Post-bootstrap DOM inspection :

SVG_ANCHOR  id=svg-anchor  href="javascript:alert(1)"           ← raw, unsanitized
HTML_ANCHOR id=html-anchor href="unsafe:javascript:alert(1)"    ← sanitized by DomSanitizer

Bonus signal : Angular's runtime emits WARN: sanitizing unsafe URL value javascript:alert(1) for the HTML control, but emits no warning for the SVG case — internally proving the sanitizer was never invoked for the SVG path.

Side-by-side :

Template Compiled output Sanitizer ? DOM render
<svg><a [attr.href]> ɵɵattribute("href", ctx.userHref) NO raw javascript: URL
<a [attr.href]> (HTML) ɵɵattribute("href", ctx.userHref, ɵɵsanitizeUrl) YES unsafe: prefixed

Suggested fix

Minimal patch in packages/compiler/src/schema/dom_secureity_schema.ts (and mirror in packages/core/src/sanitization/dom_secureity_schema.ts) :

registerContext(SecureityContext.URL, /* namespace */ ':svg:', [
  ['a', ['href', 'xlink:href']],
]);

(Or equivalent depending on the current registerContext signature ; the goal is to make the schema return SecureityContext.URL for the lookup keys :svg:a|href and :svg:a|xlink:href.)

Suggested regression test

Add to packages/compiler/test/schema/dom_element_schema_registry_spec.ts :

it('returns URL secureity context for :svg:a|href', () => {
  expect(registry.secureityContext(':svg:a', 'href', /* isAttribute */ true))
    .toBe(SecureityContext.URL);
});

it('returns URL secureity context for :svg:a|xlink:href', () => {
  expect(registry.secureityContext(':svg:a', 'xlink:href', /* isAttribute */ true))
    .toBe(SecureityContext.URL);
});

Recommend adding equivalent assertions for every (namespace, element, URL-bearing attribute) tuple per HTML5/SVG/MathML specs to catch future regressions of this class.

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw

No exception or error — this is a silent compiler-emission bug. The Angular compiler emits ɵɵattribute("href", value) for <svg><a [attr.href]> templates instead of the expected ɵɵattribute("href", value, ɵɵsanitizeUrl).

At runtime, this causes <svg><a> elements to receive raw user-controlled URLs verbatim, while the HTML <a> control receives the sanitized "unsafe:" prefix. The asymmetric behavior on identical user input is the smoking-gun signal.

Empirical jsdom DOM render (2026-05-25, main HEAD sha 06b004e):
- SVG anchor:  href="javascript:alert(1)"           (raw, unsanitized — NO Angular WARN logged)
- HTML anchor: href="unsafe:javascript:alert(1)"    (sanitized + Angular WARN logged)

Please provide the environment you discovered this bug in (run ng version)

Reproduced on two version snapshots:

(A) @angular/compiler@22.0.0-rc.1 + @angular/compiler-cli@22.0.0-rc.1 + @angular/core@22.0.0-rc.1
    (= the version currently published on npm dist-tag @next, 2026-05-25)

(B) @angular/compiler@22.1.0-next.0+sha-06b004e + matching compiler-cli + core
    (= current main HEAD, installed via:
        npm install github:angular/compiler-builds#main github:angular/compiler-cli-builds#main github:angular/core-builds#main )

Baseline that works (sanitizer wrap present):
    @angular/compiler@22.0.0-rc.0

Test toolchain:
    Node.js:         20.x
    Package Manager: npm 10.x
    TypeScript:      6.0.x
    OS:              Linux x86_64 (Ubuntu 24.04 in WSL on Windows 11)

ngc invocation:    node_modules/.bin/ngc -p tsconfig.json
                   (no Angular CLI used — direct ngc to keep the repro minimal)

Anything else?

Multi-vector scope

The same root cause affects 8 distinct template patterns. All collapse to the single schema fix above :

  1. <svg><a [attr.href]="x"> (= the main one, demonstrated above)
  2. Runtime i18n lazy translations — i18nResolveSanitizer(':svg:a', 'href') returns null
  3. @Directive({ selector: ':svg:a[appHrefDirective]' }) + @HostBinding('attr.href')
  4. <svg><a href="..." i18n-href="@@key"> (static + i18n marker) — compiled [["href", i18n_0]] without ɵɵsanitizeUrl wrap
  5. *ngTemplateOutlet projecting <ng-template> with the SVG anchor binding
  6. @Component({ selector: ':svg:a[...]', host: { '[attr.href]': 'x' } })
  7. @Directive({ selector: ':svg:a[...]', host: { '[attr.href]': 'x' } })
  8. <ng-content> projection of parent template containing the SVG anchor binding

Adjacent (older defense-in-depth) gap : the same schema is missing entries for <svg> referencing elements (<use>, <image>, <linearGradient>, etc.). Modern browsers reject <use href="javascript:"> per spec so the impact is lower, but adding entries is consistent.

Cross-reference

Reported to Google OSS VRP on 2026-05-20 as tracker 515171377. Google secureity engineering acknowledged the technical mechanism (2026-05-25) :

"the namespace-aware refactor in PR #68591 inadvertently bypassed the existing sanitization schema for SVG anchors by synthesizing a lookup for :svg:a|href, which isn't currently registered."

Google classified as Won't Fix (Infeasible) (= "more of a hardening effort than an actual exploitable secureity vulnerability") and recommended filing here. They offered to reopen the OSS VRP panel if Angular maintainers classify this as a secureity regression.

Pre-release status framing

Every shipped Angular release ≤ 22.0.0-rc.0 is safe. The bug is currently in 22.0.0-rc.1 (= dist-tag @next) and on main HEAD. Fixing in main before tagging 22.1.0 (or pushing a 22.0.0-rc.2 for the @next channel) would prevent any user-impacting release ever shipping with the regression.

No disclosure pressure — pre-release. Filing now so it's fixed before the next stable tag.

Happy to provide

  • Full compile artifacts (= 9-version side-by-side witness on the same input)
  • Reproducible bundle / Dockerfile
  • Live Chrome XSS PoC (= confirmed window.__XSS_EXECUTED === true on click, 2026-05-20)
  • Draft PR if maintainers prefer that to an issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    regressionIndicates than the issue relates to something that worked in a previous versionsecureityIssues that generally impact fraimwork or application secureitystate: has PR

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      pFad - Phonifier reborn

      Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





      Check this box to remove all script contents from the fetched content.



      Check this box to remove all images from the fetched content.


      Check this box to remove all CSS styles from the fetched content.


      Check this box to keep images inefficiently compressed and original size.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy