Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 0 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@ root = true

# Apply for all files
[*]

charset = utf-8

indent_style = space
indent_size = 2

end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
Expand Down
12 changes: 0 additions & 12 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln

# Generated files
Expand All @@ -25,13 +20,9 @@ yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like nyc and istanbul
.nyc_output
coverage
Expand All @@ -44,9 +35,6 @@ build/Release
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules

# Users Environment Variables
.lock-wscript

# macOS
*.DS_Store
.AppleDouble
Expand Down
6 changes: 4 additions & 2 deletions dist/core/rules/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions src/core/rules/form-method-require.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Listener } from '../htmlparser'
import { Rule } from '../types'

export default {
id: 'form-method-require',
description:
'The method attribute of a <form> element must be present with a valid value: "get", "post", or "dialog".',
init(parser, reporter) {
const onTagStart: Listener = (event) => {
const tagName = event.tagName.toLowerCase()

if (tagName === 'form') {
const mapAttrs = parser.getMapAttrs(event.attrs)
const col = event.col + tagName.length + 1

if (mapAttrs.method === undefined) {
reporter.warn(
'The method attribute must be present on <form> elements.',
event.line,
col,
this,
event.raw
)
} else {
const methodValue = mapAttrs.method.toLowerCase()
if (
methodValue !== 'get' &&
methodValue !== 'post' &&
methodValue !== 'dialog'
) {
reporter.warn(
'The method attribute of <form> must have a valid value: "get", "post", or "dialog".',
event.line,
col,
this,
event.raw
)
}
}
}
}

parser.addListener('tagstart', onTagStart)
},
} as Rule
1 change: 1 addition & 0 deletions src/core/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { default as buttonTypeRequire } from './button-type-require'
export { default as doctypeFirst } from './doctype-first'
export { default as doctypeHTML5 } from './doctype-html5'
export { default as emptyTagNotSelfClosed } from './empty-tag-not-self-closed'
export { default as formMethodRequire } from './form-method-require'
export { default as frameTitleRequire } from './frame-title-require'
export { default as h1Require } from './h1-require'
export { default as headScriptDisabled } from './head-script-disabled'
Expand Down
1 change: 1 addition & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface Ruleset {
'doctype-first'?: boolean
'doctype-html5'?: boolean
'empty-tag-not-self-closed'?: boolean
'form-method-require'?: boolean
'head-script-disabled'?: boolean
'href-abs-or-rel'?: 'abs' | 'rel'
'id-class-ad-disabled'?: boolean
Expand Down
64 changes: 64 additions & 0 deletions test/rules/form-method-require.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const HTMLHint = require('../../dist/htmlhint.js').HTMLHint

const ruleId = 'form-method-require'
const ruleOptions = {}

ruleOptions[ruleId] = true

describe(`Rules: ${ruleId}`, () => {
it('Form with method="get" should not result in an error', () => {
const code = '<form method="get"></form>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(0)
})

it('Form with method="post" should not result in an error', () => {
const code = '<form method="post"></form>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(0)
})

it('Form with method="dialog" should not result in an error', () => {
const code = '<form method="dialog"></form>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(0)
})

it('Form without method attribute should result in an error', () => {
const code = '<form></form>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(1)
expect(messages[0].rule.id).toBe(ruleId)
expect(messages[0].line).toBe(1)
expect(messages[0].col).toBe(6)
expect(messages[0].type).toBe('warning')
expect(messages[0].message).toBe(
'The method attribute must be present on <form> elements.'
)
})

it('Form with invalid method value should result in an error', () => {
const code = '<form method="invalid"></form>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(1)
expect(messages[0].rule.id).toBe(ruleId)
expect(messages[0].line).toBe(1)
expect(messages[0].col).toBe(6)
expect(messages[0].type).toBe('warning')
expect(messages[0].message).toBe(
'The method attribute of <form> must have a valid value: "get", "post", or "dialog".'
)
})

it('Form with uppercase method value should not result in an error', () => {
const code = '<form method="POST"></form>'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(0)
})

it('Other elements should not be affected by this rule', () => {
const code = '<div>Not a form</div><input type="text">'
const messages = HTMLHint.verify(code, ruleOptions)
expect(messages.length).toBe(0)
})
})
47 changes: 47 additions & 0 deletions website/src/content/docs/rules/form-method-require.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
id: form-method-require
title: form-method-require
description: Requires form elements to have a valid method attribute for better security and user experience.
sidebar:
badge: New
pagefind: false
hidden: true
---

import { Badge } from '@astrojs/starlight/components';

The method attribute of a `<form>` element must be present with a valid value: "get", "post", or "dialog".

Level: <Badge text="Warning" variant="caution" />

## Config value

- `true`: enable rule
- `false`: disable rule

### The following patterns are **not** considered rule violations

```html
<form method="get"></form>
<form method="post"></form>
<form method="dialog"></form>
```

### The following patterns are considered rule violations

```html
<form>No method specified</form>
<form method="invalid">Invalid method</form>
```

## Why this rule is important

The absence of the method attribute means the form will use the default `GET` method. With `GET`, form data is included in the URL (e.g., `?username=john&password=secret`), which can expose sensitive information in browser history, logs, or the network request.

The HTML specification requires that form elements have one of three valid methods:

- `get`: Appends form data to the URL (default, but not recommended for sensitive data)
- `post`: Sends form data in the request body (more secure for sensitive data)
- `dialog`: Used for dialog forms (HTML5 feature)

This rule helps ensure that forms have explicit, valid methods for better security and user experience.
Loading