Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
79 lines
2.7 KiB
JavaScript
79 lines
2.7 KiB
JavaScript
import { readFileSync } from 'node:fs'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { dirname, join } from 'node:path'
|
|
|
|
// Load the inline conditional-logic parser straight out of crm_form.html (the guest
|
|
// page's security boundary) so we test the exact code that ships, not a copy.
|
|
const html = readFileSync(
|
|
join(
|
|
dirname(fileURLToPath(import.meta.url)),
|
|
'../../../crm/www/crm_form.html',
|
|
),
|
|
'utf8',
|
|
)
|
|
const src = html
|
|
.split('// [conditional-eval:start]')[1]
|
|
.split('// [conditional-eval:end]')[0]
|
|
// eslint-disable-next-line no-new-func
|
|
const { truthy } = new Function(`${src}; return { truthy };`)()
|
|
|
|
describe('crm_form.html conditional-logic evaluator', () => {
|
|
const doc = { organization: 'Acme', first_name: 'Al', n: '10', items: [1] }
|
|
|
|
describe('correctness', () => {
|
|
it('plain fieldname → truthiness', () => {
|
|
expect(truthy('organization', doc, false)).toBe(true)
|
|
expect(truthy('missing', doc, false)).toBe(false)
|
|
})
|
|
it('eval: comparisons and logic', () => {
|
|
expect(truthy('eval:doc.organization == "Acme"', doc, false)).toBe(true)
|
|
expect(truthy('eval:doc.organization == "Other"', doc, false)).toBe(false)
|
|
expect(truthy('eval:doc.n > 5', doc, false)).toBe(true)
|
|
expect(
|
|
truthy('eval:doc.first_name && doc.organization', doc, false),
|
|
).toBe(true)
|
|
expect(truthy('eval:!doc.missing', doc, false)).toBe(true)
|
|
expect(
|
|
truthy(
|
|
'eval:(doc.a || doc.organization) && doc.first_name',
|
|
doc,
|
|
false,
|
|
),
|
|
).toBe(true)
|
|
})
|
|
it('empty/blank expression → fallback', () => {
|
|
expect(truthy('', doc, true)).toBe(true)
|
|
expect(truthy(' ', doc, false)).toBe(false)
|
|
})
|
|
it('array field → non-empty truthiness', () => {
|
|
expect(truthy('items', doc, false)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('security — never executes arbitrary JS, always falls back', () => {
|
|
const attacks = [
|
|
'eval:window',
|
|
'eval:document',
|
|
'eval:globalThis',
|
|
'eval:fetch("/x")',
|
|
'eval:alert(1)',
|
|
'eval:doc.constructor.constructor("return 1")()',
|
|
'eval:(0).constructor.constructor("return 1")()',
|
|
'eval:doc.x = 1',
|
|
]
|
|
for (const expr of attacks) {
|
|
it(`rejects ${expr}`, () => {
|
|
// any parse error → fallback is returned, so the expression never runs
|
|
expect(truthy(expr, doc, false)).toBe(false)
|
|
expect(truthy(expr, doc, true)).toBe(true)
|
|
})
|
|
}
|
|
it('cannot cause side effects on globals', () => {
|
|
globalThis.__pwned = false
|
|
truthy('eval:(globalThis.__pwned = true)', doc, false)
|
|
expect(globalThis.__pwned).toBe(false)
|
|
delete globalThis.__pwned
|
|
})
|
|
})
|
|
})
|