fix(filters): support Buffer input in base64_encode to prevent binary data corruption (#881)

* fix: support Buffer input in base64_encode filter

When binary data (e.g. images, PDFs) is passed through the template
context as a Node.js Buffer, the base64_encode filter would call
stringify() on it first, which internally does String(value). This
triggers Buffer.toString() with the default 'utf-8' encoding, which
is a lossy conversion for non-UTF-8 byte sequences — invalid bytes
get replaced with U+FFFD, permanently destroying the original data.

The fix checks for Buffer.isBuffer() before stringify, and calls
buffer.toString('base64') directly, bypassing the lossy UTF-8
intermediate step. String inputs continue through the existing path
unchanged.

Made-with: Cursor

* fix: handle Buffer in filter layer to fix browser build

Move Buffer handling from base64-impl.ts (which gets swapped for the
browser impl at build time) into base64.ts (the filter layer). This
avoids a type error during the browser rollup build where the browser
impl only accepts string.

Also guard Buffer.isBuffer() with typeof Buffer !== 'undefined' for
safety in browser environments.

Made-with: Cursor
This commit is contained in:
Tal
2026-04-23 21:30:58 +08:00
committed by GitHub
parent 30e04ba16d
commit 0ee6dbb511
2 changed files with 33 additions and 2 deletions
+5 -1
View File
@@ -8,7 +8,11 @@ import { FilterImpl } from '../template'
import { stringify } from '../util'
import { base64Encode, base64Decode } from './base64-impl'
export function base64_encode (this: FilterImpl, value: string): string {
export function base64_encode (this: FilterImpl, value: string | Buffer): string {
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
this.context.memoryLimit.use(value.byteLength)
return value.toString('base64')
}
const str = stringify(value)
this.context.memoryLimit.use(str.length)
return base64Encode(str)
+28 -1
View File
@@ -1,4 +1,4 @@
import { test } from '../../stub/render'
import { test, liquid } from '../../stub/render'
describe('filters/base64', function () {
describe('base64_encode', function () {
@@ -65,6 +65,33 @@ describe('filters/base64', function () {
})
})
describe('base64_encode with Buffer input', function () {
it('should encode a Buffer to base64 without data corruption', async () => {
const buf = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0xff, 0xfe])
const result = await liquid.parseAndRender('{{ data | base64_encode }}', { data: buf })
expect(result).toBe(buf.toString('base64'))
})
it('should preserve bytes that are invalid UTF-8', async () => {
const buf = Buffer.from([0x80, 0xff, 0xfe, 0x00, 0x01])
const result = await liquid.parseAndRender('{{ data | base64_encode }}', { data: buf })
const decoded = Buffer.from(result, 'base64')
expect(decoded).toEqual(buf)
})
it('should handle an empty Buffer', async () => {
const buf = Buffer.alloc(0)
const result = await liquid.parseAndRender('{{ data | base64_encode }}', { data: buf })
expect(result).toBe('')
})
it('should handle a Buffer containing valid UTF-8 text', async () => {
const buf = Buffer.from('Hello World', 'utf8')
const result = await liquid.parseAndRender('{{ data | base64_encode }}', { data: buf })
expect(result).toBe(Buffer.from('Hello World').toString('base64'))
})
})
describe('base64 round-trip', function () {
it('should encode and decode back to original', () => {
return test('{{ "Hello, World!" | base64_encode | base64_decode }}', 'Hello, World!')