Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
echo "EOF" >> $GITHUB_OUTPUT
- name: Create the release
if: steps.changelog.outputs.changelog_content != ''
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
name: ${{ github.ref_name }}
body: '${{ steps.changelog.outputs.changelog_content }}'
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This project adheres to [Semantic Versioning](https://semver.org/).

## 8.5.10

- Fixed XSS via unescaped `</style>` in non-bundler cases (by @TharVid).

## 8.5.9

- Speed up source map encoding paring in case of the error.
Expand Down
2 changes: 1 addition & 1 deletion lib/processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ let Root = require('./root')

class Processor {
constructor(plugins = []) {
this.version = '8.5.9'
this.version = '8.5.10'
this.plugins = this.normalize(plugins)
}

Expand Down
97 changes: 69 additions & 28 deletions lib/stringifier.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
'use strict'

// Escapes sequences that could break out of an HTML <style> context.
// Uses CSS unicode escaping (\3c = '<') which is valid CSS and parsed
// correctly by all compliant CSS consumers.
const STYLE_TAG = /(<)(\/?style\b)/gi
const COMMENT_OPEN = /(<)(!--)/g

function escapeHTMLInCSS(str) {
if (typeof str !== 'string') return str
if (!str.includes('<')) return str
return str.replace(STYLE_TAG, '\\3c $2').replace(COMMENT_OPEN, '\\3c $2')
}

const DEFAULT_RAW = {
after: '\n',
beforeClose: '\n',
Expand All @@ -25,20 +37,21 @@ class Stringifier {
}

atrule(node, semicolon) {
let raws = node.raws
let name = '@' + node.name
let params = node.params ? this.rawValue(node, 'params') : ''

if (typeof node.raws.afterName !== 'undefined') {
name += node.raws.afterName
if (typeof raws.afterName !== 'undefined') {
name += raws.afterName
} else if (params) {
name += ' '
}

if (node.nodes) {
this.block(node, name + params)
} else {
let end = (node.raws.between || '') + (semicolon ? ';' : '')
this.builder(name + params + end, node)
let end = (raws.between || '') + (semicolon ? ';' : '')
this.builder(escapeHTMLInCSS(name + params + end), node)
}
}

Expand Down Expand Up @@ -72,53 +85,77 @@ class Stringifier {
}

block(node, start) {
let between = this.raw(node, 'between', 'beforeOpen')
this.builder(start + between + '{', node, 'start')
let raws = node.raws
let between = typeof raws.between !== 'undefined'
? raws.between
: this.raw(node, 'between', 'beforeOpen')
this.builder(escapeHTMLInCSS(start + between) + '{', node, 'start')

let after
if (node.nodes && node.nodes.length) {
this.body(node)
after = this.raw(node, 'after')
after = typeof raws.after !== 'undefined'
? raws.after
: this.raw(node, 'after')
} else {
after = this.raw(node, 'after', 'emptyBody')
after = typeof raws.after !== 'undefined'
? raws.after
: this.raw(node, 'after', 'emptyBody')
}

if (after) this.builder(after)
if (after) this.builder(escapeHTMLInCSS(after))
this.builder('}', node, 'end')
}

body(node) {
let last = node.nodes.length - 1
let nodes = node.nodes
let last = nodes.length - 1
while (last > 0) {
if (node.nodes[last].type !== 'comment') break
if (nodes[last].type !== 'comment') break
last -= 1
}

let semicolon = this.raw(node, 'semicolon')
for (let i = 0; i < node.nodes.length; i++) {
let child = node.nodes[i]
let before = this.raw(child, 'before')
if (before) this.builder(before)
let isDocument = node.type === 'document'
for (let i = 0; i < nodes.length; i++) {
let child = nodes[i]
let before = child.raws.before
if (typeof before === 'undefined') {
before = this.raw(child, 'before')
}
if (before) this.builder(isDocument ? before : escapeHTMLInCSS(before))
this.stringify(child, last !== i || semicolon)
}
}

comment(node) {
let left = this.raw(node, 'left', 'commentLeft')
let right = this.raw(node, 'right', 'commentRight')
this.builder('/*' + left + node.text + right + '*/', node)
let raws = node.raws
let left = typeof raws.left !== 'undefined'
? raws.left
: this.raw(node, 'left', 'commentLeft')
let right = typeof raws.right !== 'undefined'
? raws.right
: this.raw(node, 'right', 'commentRight')
this.builder(escapeHTMLInCSS('/*' + left + node.text + right + '*/'), node)
}

decl(node, semicolon) {
let between = this.raw(node, 'between', 'colon')
let string = node.prop + between + this.rawValue(node, 'value')
let raws = node.raws
let between = typeof raws.between !== 'undefined'
? raws.between
: this.raw(node, 'between', 'colon')

let rawVal = raws.value
let value = rawVal && rawVal.value === node.value ? rawVal.raw : node.value

let string = node.prop + between + value

if (node.important) {
string += node.raws.important || ' !important'
string += raws.important || ' !important'
}

if (semicolon) string += ';'
this.builder(string, node)
this.builder(escapeHTMLInCSS(string), node)
}

document(node) {
Expand Down Expand Up @@ -154,9 +191,9 @@ class Stringifier {

// Detect style by other nodes
let root = node.root()
if (!root.rawCache) root.rawCache = {}
if (typeof root.rawCache[detect] !== 'undefined') {
return root.rawCache[detect]
let cache = root.rawCache || (root.rawCache = {})
if (typeof cache[detect] !== 'undefined') {
return cache[detect]
}

if (detect === 'before' || detect === 'after') {
Expand All @@ -175,7 +212,7 @@ class Stringifier {

if (typeof value === 'undefined') value = DEFAULT_RAW[detect]

root.rawCache[detect] = value
cache[detect] = value
return value
}

Expand Down Expand Up @@ -324,13 +361,17 @@ class Stringifier {

root(node) {
this.body(node)
if (node.raws.after) this.builder(node.raws.after)
if (node.raws.after) {
let after = node.raws.after
let isDocument = node.parent && node.parent.type === 'document'
this.builder(isDocument ? after : escapeHTMLInCSS(after))
}
}

rule(node) {
this.block(node, this.rawValue(node, 'selector'))
if (node.raws.ownSemicolon) {
this.builder(node.raws.ownSemicolon, node, 'end')
this.builder(escapeHTMLInCSS(node.raws.ownSemicolon), node, 'end')
}
}

Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "postcss",
"version": "8.5.9",
"version": "8.5.10",
"description": "Tool for transforming styles with JS plugins",
"keywords": [
"css",
Expand Down Expand Up @@ -95,10 +95,10 @@
},
"devDependencies": {
"@logux/eslint-config": "^57.1.0",
"@logux/oxc-configs": "^0.2.2",
"@size-limit/preset-small-lib": "^12.0.1",
"@types/node": "^25.5.2",
"actions-up": "^1.12.1",
"@logux/oxc-configs": "^0.3.3",
"@size-limit/preset-small-lib": "^12.1.0",
"@types/node": "^25.6.0",
"actions-up": "^1.13.0",
"c8": "^11.0.0",
"check-dts": "^0.9.0",
"clean-publish": "^6.0.5",
Expand All @@ -107,10 +107,10 @@
"multiocular": "^0.8.2",
"nanodelay": "^1.0.8",
"nanospy": "^1.0.0",
"oxfmt": "^0.43.0",
"oxfmt": "^0.45.0",
"postcss-parser-tests": "^8.9.0",
"simple-git-hooks": "^2.13.1",
"size-limit": "^12.0.1",
"size-limit": "^12.1.0",
"strip-ansi": "^6.0.1",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
Expand Down
Loading