Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
| [template-require-iframe-src-attribute](docs/rules/template-require-iframe-src-attribute.md) | require iframe elements to have src attribute | | 🔧 | |
| [template-require-splattributes](docs/rules/template-require-splattributes.md) | require splattributes usage in component templates | | | |
| [template-require-strict-mode](docs/rules/template-require-strict-mode.md) | require templates to be in strict mode | | | |
| [template-require-valid-named-block-naming-format](docs/rules/template-require-valid-named-block-naming-format.md) | require valid named block naming format | | 🔧 | |
Expand Down
84 changes: 84 additions & 0 deletions docs/rules/template-require-iframe-src-attribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# ember/template-require-iframe-src-attribute

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

Omitting the `src` attribute from an `<iframe>` element can silently bypass your Content Security Policy's `frame-src` directive.

When an `<iframe>` has no `src` (or an empty `src`), it implicitly loads `about:blank`. This document inherits the origin of the parent page, allowing the iframe to operate under the same-origin policy. Later dynamically setting `src` (e.g., via JavaScript) does not re-validate against `frame-src`, which exposes an **elevation-of-privilege vector**.

This rule ensures that all `<iframe>` elements specify a `src` attribute explicitly in the markup, even if it is a placeholder like `"about:blank"` or a safe data URL.

## 🚨 Why this matters

An attacker could inject a seemingly harmless `<iframe>` into your template, then programmatically change its `src`. Without a defined `src` at load time, the browser grants it origin privileges that persist **after the `src` is changed**, effectively sidestepping CSP.

## Examples

This rule **forbids** the following:

```gjs
<template>
<iframe></iframe>
</template>
```

```gjs
<template>
<iframe {{this.setFrameElement}}></iframe>
</template>
```

This rule **allows** the following:

```gjs
<template>
<iframe src='about:blank'></iframe>
</template>
```

```gjs
<template>
<iframe src='/safe-path' {{this.setFrameElement}}></iframe>
</template>
```

```gjs
<template>
<iframe src='data:text/html,<h1>safe</h1>'></iframe>
</template>
```

```gjs
<template>
<iframe src=''></iframe>
</template>
```

## Migration

If you're dynamically setting the `src`, pre-populate the element with a secure initial `src` to ensure CSP applies:

```gjs
<template>
<iframe src='about:blank' {{this.setFrameElement}}></iframe>
</template>
```

Or, if you know the eventual value ahead of time:

```gjs
<template>
<iframe src='/iframe-entry' {{this.setFrameElement}}></iframe>
</template>
```

## Related Rules

- [require-iframe-title](template-require-iframe-title.md)

## References

- [CSP `frame-src` bypass via missing `src`](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-src)
- [MDN on `<iframe>` `src`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-src)
70 changes: 70 additions & 0 deletions lib/rules/template-require-iframe-src-attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const ERROR_MESSAGE =
'Security Risk: `<iframe>` must include a static `src` attribute. Otherwise, CSP `frame-src` is bypassed and `about:blank` inherits parent origin, creating an elevated-privilege frame.';
const FIXED_SRC_ATTRIBUTE = 'src="about:blank"';

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'require iframe elements to have src attribute',
category: 'Best Practices',
recommended: false,
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-require-iframe-src-attribute.md',
templateMode: 'both',
},
fixable: 'code',
schema: [],
messages: {
requireSrc: ERROR_MESSAGE,
},
originallyFrom: {
name: 'ember-template-lint',
rule: 'lib/rules/require-iframe-src-attribute.js',
docs: 'docs/rule/require-iframe-src-attribute.md',
tests: 'test/unit/rules/require-iframe-src-attribute-test.js',
},
},

create(context) {
return {
GlimmerElementNode(node) {
if (node.tag !== 'iframe') {
return;
}

const hasSrcAttribute = node.attributes.find((attr) => attr.name === 'src');

if (!hasSrcAttribute) {
context.report({
node,
messageId: 'requireSrc',
fix(fixer) {
const firstModifier = node.modifiers[0];

if (firstModifier) {
return fixer.insertTextBeforeRange(
[firstModifier.range[0], firstModifier.range[0]],
`${FIXED_SRC_ATTRIBUTE} `
);
}

const lastAttribute = node.attributes.at(-1);

if (lastAttribute) {
return fixer.insertTextAfterRange(lastAttribute.range, ` ${FIXED_SRC_ATTRIBUTE}`);
}

const tagNameEnd = node.parts.at(-1)?.range[1] ?? node.range[0] + '<iframe'.length;

return fixer.insertTextAfterRange(
[tagNameEnd, tagNameEnd],
` ${FIXED_SRC_ATTRIBUTE}`
);
},
});
}
},
};
},
};
94 changes: 94 additions & 0 deletions tests/lib/rules/template-require-iframe-src-attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const rule = require('../../../lib/rules/template-require-iframe-src-attribute');
const RuleTester = require('eslint').RuleTester;

const ERROR_MESSAGE =
'Security Risk: `<iframe>` must include a static `src` attribute. Otherwise, CSP `frame-src` is bypassed and `about:blank` inherits parent origin, creating an elevated-privilege frame.';

const ruleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser'),
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
});

ruleTester.run('template-require-iframe-src-attribute', rule, {
valid: [
'<template><iframe src="about:blank"></iframe></template>',
'<template><iframe src="/safe-path" {{this.setFrameElement}}></iframe></template>',
'<template><iframe src="data:text/html,<h1>safe</h1>"></iframe></template>',
'<template><iframe src=""></iframe></template>',
],
invalid: [
{
code: '<template><iframe {{this.setFrameElement}}></iframe></template>',
output: '<template><iframe src="about:blank" {{this.setFrameElement}}></iframe></template>',
errors: [
{
message: ERROR_MESSAGE,
},
],
},
{
code: '<template><iframe></iframe></template>',
output: '<template><iframe src="about:blank"></iframe></template>',
errors: [
{
message: ERROR_MESSAGE,
},
],
},
{
code: '<template><iframe ...attributes id="foo"></iframe></template>',
output: '<template><iframe ...attributes id="foo" src="about:blank"></iframe></template>',
errors: [
{
message: ERROR_MESSAGE,
},
],
},
],
});

const hbsRuleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser/hbs'),
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
});

hbsRuleTester.run('template-require-iframe-src-attribute', rule, {
valid: [
'<iframe src="about:blank"></iframe>',
'<iframe src="/safe-path" {{this.setFrameElement}}></iframe>',
'<iframe src="data:text/html,<h1>safe</h1>"></iframe>',
'<iframe src=""></iframe>',
],
invalid: [
{
code: '<iframe {{this.setFrameElement}}></iframe>',
output: '<iframe src="about:blank" {{this.setFrameElement}}></iframe>',
errors: [
{
message: ERROR_MESSAGE,
},
],
},
{
code: '<iframe></iframe>',
output: '<iframe src="about:blank"></iframe>',
errors: [
{
message: ERROR_MESSAGE,
},
],
},
{
code: '<iframe ...attributes id="foo"></iframe>',
output: '<iframe ...attributes id="foo" src="about:blank"></iframe>',
errors: [
{
message: ERROR_MESSAGE,
},
],
},
],
});
Loading