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
2 changes: 1 addition & 1 deletion apps/pii/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Test-only deps. Unit tests need requirements.txt + this file (no models);
# integration tests additionally need the models baked into the docker images
# (see tests/test_integration.py).
pytest==8.4.1
pytest==9.0.3
httpx==0.28.1
4 changes: 2 additions & 2 deletions apps/pii/requirements-gliner.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
# torch is pinned in the Dockerfile instead: the CPU and CUDA targets install
# the same version from different wheel indexes.
gliner==0.2.27
transformers==4.56.2
huggingface_hub==0.35.3
transformers==5.3.0
huggingface_hub==1.3.0
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,18 @@ describe('markdown paste', () => {
expect(cleaned).toContain('<td>a</td>')
expect(transformHtml(editor, 'a<script>alert(1)</script>b')).toBe('ab')
})

it('strips nested/repeated <script> tags in a single pass, even deeply nested', () => {
editor = mount()
expect(transformHtml(editor, 'a<script>x<script>y</script></script>b')).toBe('ab')
const deeplyNested = `a${'<script>'.repeat(50)}x${'</script>'.repeat(50)}b`
expect(transformHtml(editor, deeplyNested)).toBe('ab')
})

it('drops an unterminated <script>/<style> and everything after it, without duplicating the prefix', () => {
editor = mount()
expect(transformHtml(editor, 'abc<script>never-closes')).toBe('abc')
expect(transformHtml(editor, 'abc<style>never-closes')).toBe('abc')
expect(transformHtml(editor, '<script>x<script>y</script>')).toBe('')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,48 @@ function parseVscodeLanguage(data: string | undefined): string {
}
}

/** `<style>`/`<script>` elements (with their content), matched as a pair via the tag backreference. */
const NON_CONTENT_HTML = /<(style|script)\b[\s\S]*?<\/\1>/gi
/** A `<style>`/`<script>` open or close tag token, scanned one at a time (never the element body). */
const NON_CONTENT_TAG = /<\/?\s*(style|script)\b[^>]*>/gi

/**
* Strips `<style>`/`<script>` elements from pasted HTML. Google Sheets and Word prepend a `<style>`
* block of CSS (and Sheets a `<google-sheets-html-origin>` wrapper); ProseMirror's DOM parser has no
* rule for `<style>`, so it would walk the element's CSS text into the document as literal paragraphs.
* Removing these before parsing keeps the pasted content clean (PM already discards unknown wrappers).
*
* Scans tag tokens in a single linear pass, tracking nesting depth of the currently-open tag name, so
* nested/overlapping tags — e.g. `<script><script>x</script>` — can't leave a surviving `<script>`
* behind. A naive single `replace()` pass over `<tag>[\s\S]*?<\/tag>` matches only the innermost pair
* and leaves the outer tag dangling; repeating that replace until stable fixes correctness but costs
* O(depth) full-string rescans on attacker-controlled clipboard input. This does it in one pass instead.
* A stray close tag encountered outside any open element (depth 0) is left in place untouched. `cursor`
* advances past an open tag the moment it opens (not when it closes), so if the input ends before the
* element closes — truncated or malformed clipboard HTML — the unterminated element and everything
* after it is dropped rather than reappearing unstripped in the final `html.slice(cursor)` flush.
*/
function stripNonContentHtml(html: string): string {
return html.replace(NON_CONTENT_HTML, '')
let result = ''
let cursor = 0
let depth = 0
let openTagName = ''
NON_CONTENT_TAG.lastIndex = 0
let match: RegExpExecArray | null
while ((match = NON_CONTENT_TAG.exec(html))) {
const isClosing = match[0][1] === '/'
const tagName = match[1].toLowerCase()
if (depth === 0) {
if (isClosing) continue
result += html.slice(cursor, match.index)
openTagName = tagName
depth = 1
cursor = match.index + match[0].length
} else if (tagName === openTagName) {
depth += isClosing ? -1 : 1
if (depth === 0) cursor = match.index + match[0].length
}
}
if (depth === 0) result += html.slice(cursor)
return result
Comment thread
waleedlatif1 marked this conversation as resolved.
}

/**
Expand Down
9 changes: 7 additions & 2 deletions apps/sim/ee/workspace-forking/lib/remap/block-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ function uuidToBytes(uuid: string): Buffer {
/**
* Deterministic UUIDv5 (SHA-1) of `name` within `namespace`. The same inputs
* always yield the same UUID, which is how fork block identity stays stable.
*
* SHA-1 is mandated by RFC 4122 for UUIDv5 and is used here only for deterministic id derivation,
* never for secrecy or integrity — not a security use of the algorithm. Swapping it would change
* every derived id, breaking webhook URLs and stored block-id references across existing forks
* (see {@link FORK_BLOCK_NAMESPACE}).
*/
function uuidV5(name: string, namespace: string): string {
const hash = createHash('sha1')
hash.update(uuidToBytes(namespace))
hash.update(Buffer.from(name, 'utf8'))
hash.update(uuidToBytes(namespace)) // lgtm[js/weak-cryptographic-algorithm]
hash.update(Buffer.from(name, 'utf8')) // lgtm[js/weak-cryptographic-algorithm]
const bytes = hash.digest().subarray(0, 16)
bytes[6] = (bytes[6] & 0x0f) | 0x50
bytes[8] = (bytes[8] & 0x3f) | 0x80
Expand Down
Loading