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
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)
})
})
2 changes: 2 additions & 0 deletions website/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export default defineConfig({
attrs: {
rel: 'manifest',
href: '/site.webmanifest',
crossorigin: 'use-credentials',
fetchpriority: 'low',
},
},
{
Expand Down
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
hidden: true
pagefind: false
---

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