Skip to content

Linter: Implement html-require-script-nonce rule#1384

Merged
marcoroth merged 1 commit intomarcoroth:mainfrom
markokajzer:marko/requirescriptnonce
Mar 21, 2026
Merged

Linter: Implement html-require-script-nonce rule#1384
marcoroth merged 1 commit intomarcoroth:mainfrom
markokajzer:marko/requirescriptnonce

Conversation

@markokajzer
Copy link
Contributor

@markokajzer markokajzer commented Mar 15, 2026

closes #543

handles three things

  1. html: <script> tags
  2. erb: Rails javascript helpers, i.e. javascript_tag, javascript_include_tag, and javascript_pack_tag
  3. erb: Rails tag helpers, i.e. tag.script

uses #1374 to handle both <script> and javascript_tag helpers via BaseRuleVisitor#visitHTMLElementNode.

Note

Does not handle nil values, e.g. tag.script nonce: nil based on the specs of the inspiration. If wanted, we can add more specs and handlers.

@github-actions github-actions bot added documentation Improvements or additions to documentation linter typescript linter-rule labels Mar 15, 2026
@markokajzer markokajzer force-pushed the marko/requirescriptnonce branch from 89a0241 to aacd635 Compare March 15, 2026 19:00
Copy link
Owner

@marcoroth marcoroth left a comment

Choose a reason for hiding this comment

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

Hey @markokajzer, thanks for working on this rule!

I think we want to wait for #1374, so that we can write the linter rule by just operating on HTMLElementNodes.

And then, we can probably also rename the rule to be html-* prefixed.

@marcoroth
Copy link
Owner

Just merged #1374 🙌🏼

@markokajzer
Copy link
Contributor Author

Thank you for your tireless work @marcoroth

I'll rebase, and give it another shot! 👍

@markokajzer markokajzer force-pushed the marko/requirescriptnonce branch from aacd635 to 5709c8d Compare March 21, 2026 00:45

get defaultConfig(): FullRuleConfig {
return {
enabled: true,
Copy link
Contributor Author

@markokajzer markokajzer Mar 21, 2026

Choose a reason for hiding this comment

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

just noticed it's not enabled by default in erb_lint. do we want the same here?

Copy link
Owner

Choose a reason for hiding this comment

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

I think its fine to keep this one enabled for now 🙌🏼

get defaultConfig(): FullRuleConfig {
return {
enabled: true,
severity: "error"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

what's the correct severity?

Copy link
Owner

Choose a reason for hiding this comment

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

We still haven't really balanced all the severities yet. So I think we can keep this one as error for now, even though it might be a bit harsh/aggressive.

@markokajzer markokajzer requested a review from marcoroth March 21, 2026 00:47
@markokajzer
Copy link
Contributor Author

markokajzer commented Mar 21, 2026

@marcoroth I think I got it now! Given that #1374 did not include parsing of javascript_pack_tag, I removed the handling of it here as well, so that we can simplify and only rely on visitHTMLElementNode. (I think it's also deprecated / removed in Rails as well nowadays?)

Let me know if any other changes are necessary, I wasn't sure about severity and whether we want to enable by default! 🙏

@markokajzer markokajzer force-pushed the marko/requirescriptnonce branch from 5709c8d to 93aa945 Compare March 21, 2026 00:53
@markokajzer markokajzer changed the title Linter: Implement erb-require-script-nonce rule Linter: Implement html-require-script-nonce rule Mar 21, 2026
Copy link
Owner

@marcoroth marcoroth left a comment

Choose a reason for hiding this comment

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

Awesome, thank you @markokajzer! I think this looks great!

Given the way the rule is structured, it should also be able to catch javascript_pack_tag once we add support for it in the parser. So I think it's fine to leave that out!

Thank you! 🙏🏼


get defaultConfig(): FullRuleConfig {
return {
enabled: true,
Copy link
Owner

Choose a reason for hiding this comment

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

I think its fine to keep this one enabled for now 🙌🏼

get defaultConfig(): FullRuleConfig {
return {
enabled: true,
severity: "error"
Copy link
Owner

Choose a reason for hiding this comment

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

We still haven't really balanced all the severities yet. So I think we can keep this one as error for now, even though it might be a bit harsh/aggressive.

@marcoroth marcoroth merged commit bc2eb93 into marcoroth:main Mar 21, 2026
12 checks passed
@marcoroth
Copy link
Owner

Implemented a small follow up on #1452 🙌🏼

marcoroth added a commit that referenced this pull request Mar 22, 2026
In Rails, `nonce: true` on `javascript_include_tag` and `javascript_tag`
resolve to `content_security_policy_nonce` at runtime, and `nonce:
false` omits the attribute. Other helpers like `tag.script` and
`content_tag` pass it through as a literal value.

Previously, `javascript_include_tag` and `javascript_tag` also just
passed along and transformed the `nonce` value as a literal value.

This pulls request adds `resolve_nonce_attribute()` to the parser to
handle this transformation for `javascript_include_tag` and
`javascript_tag`.

So a document like:
```erb
<%= javascript_tag nonce: true do %>
  alert('Hello')
<% end %>
```

Now properly parses as:
```diff
@ DocumentNode (location: (1:0)-(4:0))
└── children: (2 items)
    ├── @ HTMLElementNode (location: (1:0)-(3:9))
    │   ├── open_tag:
    │   │   └── @ ERBOpenTagNode (location: (1:0)-(1:36))
    │   │       ├── tag_opening: "<%=" (location: (1:0)-(1:3))
    │   │       ├── content: " javascript_tag nonce: true do " (location: (1:3)-(1:34))
    │   │       ├── tag_closing: "%>" (location: (1:34)-(1:36))
    │   │       ├── tag_name: "script" (location: (1:4)-(1:18))
    │   │       └── children: (1 item)
    │   │           └── @ HTMLAttributeNode (location: (1:19)-(1:30))
    │   │               ├── name:
    │   │               │   └── @ HTMLAttributeNameNode (location: (1:19)-(1:24))
    │   │               │       └── children: (1 item)
    │   │               │           └── @ LiteralNode (location: (1:19)-(1:24))
    │   │               │               └── content: "nonce"
    │   │               │
    │   │               ├── equals: ": " (location: (1:24)-(1:26))
    │   │               └── value:
    │   │                   └── @ HTMLAttributeValueNode (location: (1:26)-(1:30))
    │   │                       ├── open_quote: ∅
    │   │                       ├── children: (1 item)
-   │   │                       │   └── @ LiteralNode (location: (1:26)-(1:30))
-   │   │                       │       └── content: "true"
+   │   │                       │   └── @ RubyLiteralNode (location: (1:26)-(1:30))
+   │   │                       │       └── content: "content_security_policy_nonce"
    │   │                       │
    │   │                       ├── close_quote: ∅
    │   │                       └── quoted: false
    │   │
    │   ├── tag_name: "script" (location: (1:4)-(1:18))
    │   ├── body: (1 item)
    │   │   └── @ LiteralNode (location: (1:36)-(3:0))
    │   │       └── content: "\n  alert('Hello')\n"
    │   │
    │   ├── close_tag:
    │   │   └── @ ERBEndNode (location: (3:0)-(3:9))
    │   │       ├── tag_opening: "<%" (location: (3:0)-(3:2))
    │   │       ├── content: " end " (location: (3:2)-(3:7))
    │   │       └── tag_closing: "%>" (location: (3:7)-(3:9))
    │   │
    │   ├── is_void: false
    │   └── element_source: "ActionView::Helpers::JavaScriptHelper#javascript_tag"
    │
    └── @ HTMLTextNode (location: (3:9)-(4:0))
        └── content: "\n"
```

So that it can get properly transformed from/to:
```erb
<script nonce="<%= content_security_policy_nonce %>">
  alert('Hello')
</script>
```

Additionally, it updates the `html-require-script-nonce` linter rule to
flag `tag.script` and `content_tag :script` using `nonce: true` or
`nonce: false`, since those produce literal attribute values that will
not match the CSP header. The rule now flags the following:
```erb
<%= tag.script nonce: true do %>
  alert('Hello')
<% end %>
```

with:
```txt
`nonce: true` on `tag.script` outputs a literal `nonce="true"` attribute, which will not match the Content Security Policy header and the browser will block the script. Only `javascript_tag` and `javascript_include_tag` resolve `nonce: true` to the per-request `content_security_policy_nonce`. Use `javascript_tag` with `nonce: true` instead.
```

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

Labels

documentation Improvements or additions to documentation linter linter-rule typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

erb-lint: RequireScriptNonce

2 participants