├── .gitignore ├── spec ├── spec_helper.cr └── xssmaze_spec.cr ├── images └── showcase.png ├── src ├── protections │ └── protections.cr ├── maze.cr ├── mazes │ ├── jf_xss.cr │ ├── path.cr │ ├── header.cr │ ├── redirect.cr │ ├── hidden_xss.cr │ ├── inframe_xss.cr │ ├── post.cr │ ├── basic.cr │ ├── decode.cr │ ├── inattr_xss.cr │ ├── injs_xss.cr │ ├── svg_xss.cr │ ├── event_handler.cr │ ├── csp_bypass.cr │ ├── json_xss.cr │ ├── css_injection.cr │ ├── template_injection.cr │ ├── websocket_xss.cr │ ├── advanced_xss.cr │ └── dom.cr ├── banner.cr └── xssmaze.cr ├── .editorconfig ├── shard.yml ├── CONTRIBUTING.md ├── Dockerfile ├── shard.lock ├── .github ├── workflows │ ├── crystal_build.yml │ ├── crystal_lint.yml │ ├── noir.yml │ ├── release_sbom.yml │ └── ghcr.yml └── copilot-instructions.md ├── Dockerfile.arm ├── .ameba.yml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | *.dwarf 6 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/xssmaze" 3 | -------------------------------------------------------------------------------- /images/showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahwul/xssmaze/HEAD/images/showcase.png -------------------------------------------------------------------------------- /spec/xssmaze_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Xssmaze do 4 | # TODO: Write tests 5 | 6 | it "works" do 7 | false.should eq(true) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /src/protections/protections.cr: -------------------------------------------------------------------------------- 1 | module XSSProtection 2 | def self.escape_level1(str) 3 | str 4 | end 5 | 6 | def self.escape_level2(str) 7 | str 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: xssmaze 2 | version: 0.1.0 3 | 4 | authors: 5 | - hahwul 6 | 7 | targets: 8 | xssmaze: 9 | main: src/xssmaze.cr 10 | 11 | dependencies: 12 | kemal: 13 | github: kemalcr/kemal 14 | 15 | crystal: 1.8.2 16 | 17 | license: MIT 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 1. Fork this repository. 3 | 2. Clone the forked repository to your local environment. 4 | 3. Make your changes and push them to your forked repository. 5 | 4. Submit a pull request to this repository with a detailed explanation of your changes. 6 | 7 | Thank you for your contribution! 8 | -------------------------------------------------------------------------------- /src/maze.cr: -------------------------------------------------------------------------------- 1 | class Maze 2 | @name : String 3 | @url : String 4 | @desc : String 5 | 6 | def initialize(@name, @url, @desc) 7 | end 8 | 9 | macro define_getter_methods(names) 10 | {% for name, index in names %} 11 | def {{name.id}} 12 | @{{name.id}} 13 | end 14 | {% end %} 15 | end 16 | 17 | define_getter_methods [name, url, desc] 18 | end 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # BUILDER 2 | FROM crystallang/crystal:latest-alpine As builder 3 | 4 | WORKDIR /xssmaze 5 | COPY . . 6 | 7 | RUN shards install 8 | RUN shards build --release --no-debug --production 9 | 10 | # RUNNER 11 | FROM alpine 12 | USER 2:2 13 | 14 | COPY --from=builder /xssmaze/bin/xssmaze /app/xssmaze 15 | COPY --from=builder /etc/ssl/cert.pem /etc/ssl/ 16 | 17 | WORKDIR /app/ 18 | 19 | CMD ["/app/xssmaze"] 20 | -------------------------------------------------------------------------------- /shard.lock: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | shards: 3 | backtracer: 4 | git: https://github.com/sija/backtracer.cr.git 5 | version: 1.2.4 6 | 7 | exception_page: 8 | git: https://github.com/crystal-loot/exception_page.git 9 | version: 0.5.0 10 | 11 | kemal: 12 | git: https://github.com/kemalcr/kemal.git 13 | version: 1.7.3 14 | 15 | radix: 16 | git: https://github.com/luislavena/radix.git 17 | version: 0.4.1 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/crystal_build.yml: -------------------------------------------------------------------------------- 1 | name: Crystal CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | container: 15 | image: crystallang/crystal 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Install dependencies 20 | run: shards install 21 | - name: Build 22 | run: shards build 23 | -------------------------------------------------------------------------------- /src/mazes/jf_xss.cr: -------------------------------------------------------------------------------- 1 | def load_jf_xss 2 | Xssmaze.push("jf-xss-level1", "/jf/level1/?query=a", "escape a-Z") 3 | get "/jf/level1/" do |env| 4 | query = env.params.query["query"] 5 | 6 | "" 9 | end 10 | get "/jf/level1" do |env| 11 | query = env.params.query["query"] 12 | 13 | "" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.github/workflows/crystal_lint.yml: -------------------------------------------------------------------------------- 1 | name: Crystal Lint 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | container: 13 | image: crystallang/crystal 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Crystal Ameba Linter 18 | id: crystal-ameba 19 | uses: crystal-ameba/github-action@v0.8.0 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /Dockerfile.arm: -------------------------------------------------------------------------------- 1 | # BUILDER 2 | FROM 84codes/crystal:latest AS builder 3 | 4 | WORKDIR /xssmaze 5 | COPY . . 6 | 7 | RUN shards install 8 | RUN shards build --release --no-debug --production 9 | 10 | # RUNNER 11 | FROM arm64v8/alpine:latest 12 | 13 | RUN apk add --no-cache libgcc pcre2 libstdc++ libc6-compat 14 | 15 | COPY --from=builder /xssmaze/bin/xssmaze /app/xssmaze 16 | COPY --from=builder /etc/ssl/cert.pem /etc/ssl/ 17 | 18 | WORKDIR /app/ 19 | 20 | CMD ["/app/xssmaze"] -------------------------------------------------------------------------------- /.github/workflows/noir.yml: -------------------------------------------------------------------------------- 1 | name: Security Analysis 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | noir-analysis: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v5 12 | 13 | - name: Run OWASP Noir 14 | id: noir 15 | uses: owasp-noir/noir@v0.24.0 16 | with: 17 | base_path: "." 18 | 19 | - name: Display results 20 | run: echo '${{ steps.noir.outputs.endpoints }}' | jq . 21 | -------------------------------------------------------------------------------- /src/banner.cr: -------------------------------------------------------------------------------- 1 | def banner 2 | art = <<-ASCII 3 | __ __ ___ ___ __ __ 4 | \\ \\/ / / __| / __| | \\/ | __ _ ___ ___ 5 | > < \\__ \\ \\__ \\ | |\\/| | / _` | |_ / / -_) 6 | /_/\\_\\ |___/ |___/ |_|__|_| \\__,_| _/__| \\___| 7 | _|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""| 8 | `-0-0-' `-0-0-' `-0-0-' `-0-0-' `-0-0-' `-0-0-' 9 | ASCII 10 | 11 | puts art 12 | puts "----------------------------------------------------------" 13 | end 14 | -------------------------------------------------------------------------------- /.ameba.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was generated by `ameba --gen-config` 2 | # on 2025-11-15 13:40:11 UTC using Ameba version 1.5.0. 3 | # The point is for the user to remove these configuration records 4 | # one by one as the reported problems are removed from the code base. 5 | 6 | # Problems found: 1 7 | # Run `ameba --only Metrics/CyclomaticComplexity` for details 8 | Metrics/CyclomaticComplexity: 9 | Description: Disallows methods with a cyclomatic complexity higher than `MaxComplexity` 10 | MaxComplexity: 10 11 | Excluded: 12 | - src/mazes/decode.cr 13 | Enabled: true 14 | Severity: Warning 15 | -------------------------------------------------------------------------------- /src/mazes/path.cr: -------------------------------------------------------------------------------- 1 | def load_path 2 | Xssmaze.push("path-level1", "/path/level1/a", "reflected") 3 | get "/path/level1/:name" do |env| 4 | name = env.params.url["name"] 5 | "Hi #{name}" 6 | end 7 | 8 | Xssmaze.push("path-level2", "/path/level2/a", "escape to %2f") 9 | get "/path/level2/:name" do |env| 10 | name = env.params.url["name"].gsub("%2f", "") 11 | "Hi #{name}" 12 | end 13 | 14 | Xssmaze.push("path-level3", "/path/level3/a", "escape to %20") 15 | get "/path/level3/:name" do |env| 16 | name = env.params.url["name"].gsub(" ", "").gsub("%20", "") 17 | "Hi #{name}" 18 | end 19 | 20 | Xssmaze.push("path-level4", "/path/level4/a", "escape to %2f and %20") 21 | get "/path/level4/:name" do |env| 22 | name = env.params.url["name"].gsub("%2f", "").gsub("%20", "") 23 | "Hi #{name}" 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /src/mazes/header.cr: -------------------------------------------------------------------------------- 1 | def load_header 2 | Xssmaze.push("header-level1", "/header/level1/", "referer header") 3 | get "/header/level1/" do |env| 4 | env.response.headers["referer"] 5 | end 6 | get "/header/level1" do |env| 7 | env.response.headers["referer"] 8 | end 9 | 10 | Xssmaze.push("header-level2", "/header/level2/", "user-agent header") 11 | get "/header/level2/" do |env| 12 | env.response.headers["user-agent"] 13 | end 14 | get "/header/level2" do |env| 15 | env.response.headers["user-agent"] 16 | end 17 | 18 | Xssmaze.push("header-level3", "/header/level3/", "authorization header") 19 | get "/header/level3/" do |env| 20 | env.response.headers["authorization"] 21 | end 22 | get "/header/level3" do |env| 23 | env.response.headers["authorization"] 24 | end 25 | 26 | Xssmaze.push("header-level4", "/header/level4/", "cookie header") 27 | get "/header/level4/" do |env| 28 | env.response.headers["cookie"] 29 | end 30 | get "/header/level4" do |env| 31 | env.response.headers["cookie"] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /.github/workflows/release_sbom.yml: -------------------------------------------------------------------------------- 1 | name: Generate and Upload SBOM 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | generate-sbom: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | steps: 13 | # Checkout the repository code 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | # Generate SBOM using hahwul/cyclonedx-cr action 18 | - name: Generate SBOM 19 | uses: hahwul/cyclonedx-cr@v1.0.0 20 | with: 21 | shard_file: ./shard.yml # Explicitly map to shard_file 22 | lock_file: ./shard.lock # Explicitly map to lock_file 23 | output_file: ./sbom.xml # Map to output_file 24 | output_format: xml # Map to output_format 25 | spec_version: 1.6 # Optional, specify if needed 26 | 27 | # Upload SBOM to GitHub Release 28 | - name: Upload SBOM to Release 29 | uses: softprops/action-gh-release@v2 30 | with: 31 | files: ./sbom.xml 32 | token: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 hahwul 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/mazes/redirect.cr: -------------------------------------------------------------------------------- 1 | def load_redirect 2 | Xssmaze.push("redirect-level1", "/redirect/level1/", "query param") 3 | get "/redirect/level1/" do |env| 4 | env.redirect env.params.query["query"] 5 | end 6 | get "/redirect/level1" do |env| 7 | env.redirect env.params.query["query"] 8 | end 9 | 10 | Xssmaze.push("redirect-level2", "/redirect/level2/", "query param") 11 | get "/redirect/level2/" do |env| 12 | env.redirect env.params.query["query"].gsub("javascript", "") 13 | end 14 | get "/redirect/level2" do |env| 15 | env.redirect env.params.query["query"].gsub("javascript", "") 16 | end 17 | 18 | Xssmaze.push("redirect-level3", "/redirect/level3/", "query param") 19 | get "/redirect/level3/" do |env| 20 | env.redirect env.params.query["query"].downcase.gsub("javascript", "") 21 | end 22 | get "/redirect/level3" do |env| 23 | env.redirect env.params.query["query"].downcase.gsub("javascript", "") 24 | end 25 | 26 | Xssmaze.push("redirect-level4", "/redirect/level4/", "query param") 27 | get "/redirect/level4/" do |env| 28 | env.redirect env.params.query["query"].downcase.gsub("javascript", "").downcase.gsub("javascript", "") 29 | end 30 | get "/redirect/level4" do |env| 31 | env.redirect env.params.query["query"].downcase.gsub("javascript", "").downcase.gsub("javascript", "") 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /src/mazes/hidden_xss.cr: -------------------------------------------------------------------------------- 1 | def load_hidden_xss 2 | Xssmaze.push("hidden-xss-level1", "/hidden/level1/?query=a", "input-hidden") 3 | get "/hidden/level1/" do |env| 4 | query = env.params.query["query"] 5 | 6 | "" 7 | end 8 | get "/hidden/level1" do |env| 9 | query = env.params.query["query"] 10 | 11 | "" 12 | end 13 | 14 | Xssmaze.push("hidden-xss-level2", "/hidden/level2/?query=a", "input-hidden and escape < >") 15 | get "/hidden/level2/" do |env| 16 | query = env.params.query["query"] 17 | query = query.gsub("<", "").gsub(">", "") 18 | 19 | "" 20 | end 21 | get "/hidden/level2" do |env| 22 | query = env.params.query["query"] 23 | query = query.gsub("<", "").gsub(">", "") 24 | 25 | "" 26 | end 27 | 28 | Xssmaze.push("hidden-xss-level3", "/hidden/level3/?query=a", "input-hidden and escape < > and space") 29 | get "/hidden/level3/" do |env| 30 | query = env.params.query["query"] 31 | query = query.gsub("<", "").gsub(">", "").gsub(" ", "") 32 | 33 | "" 34 | end 35 | get "/hidden/level3" do |env| 36 | query = env.params.query["query"] 37 | query = query.gsub("<", "").gsub(">", "").gsub(" ", "") 38 | 39 | "" 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/mazes/inframe_xss.cr: -------------------------------------------------------------------------------- 1 | def load_inframe_xss 2 | Xssmaze.push("inframe-xss-level1", "/inframe/level1/?url=a", "src attribute in iframe tag") 3 | get "/inframe/level1/" do |env| 4 | query = env.params.query["url"] 5 | 6 | "" 7 | end 8 | get "/inframe/level1" do |env| 9 | query = env.params.query["url"] 10 | 11 | "" 12 | end 13 | 14 | Xssmaze.push("inframe-xss-level2", "/inframe/level2/?url=a", "src attribute in iframe tag") 15 | get "/inframe/level2/" do |env| 16 | query = env.params.query["url"] 17 | 18 | "" 19 | end 20 | get "/inframe/level2" do |env| 21 | query = env.params.query["url"] 22 | 23 | "" 24 | end 25 | 26 | Xssmaze.push("inframe-xss-level3", "/inframe/level3/?url=a", "src attribute in iframe tag") 27 | get "/inframe/level3/" do |env| 28 | query = env.params.query["url"] 29 | 30 | "" 31 | end 32 | get "/inframe/level3" do |env| 33 | query = env.params.query["url"] 34 | 35 | "" 36 | end 37 | 38 | Xssmaze.push("inframe-xss-level4", "/inframe/level4/?url=a", "src attribute in iframe tag") 39 | get "/inframe/level4/" do |env| 40 | query = env.params.query["url"] 41 | 42 | "" 43 | end 44 | get "/inframe/level4" do |env| 45 | query = env.params.query["url"] 46 | 47 | "" 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /src/mazes/post.cr: -------------------------------------------------------------------------------- 1 | def load_post 2 | Xssmaze.push("post-level1", "/post/level1/", "POST-Form => 'query=a'") 3 | get "/post/level1/" do |_| 4 | "
" 5 | end 6 | get "/post/level1" do |_| 7 | "
" 8 | end 9 | post "/post/level1/" do |env| 10 | query = env.params.body["query"].as(String) 11 | "query: #{query}" 12 | end 13 | post "/post/level1" do |env| 14 | query = env.params.body["query"].as(String) 15 | "query: #{query}" 16 | end 17 | 18 | Xssmaze.push("post-level2", "/post/level2/", "POST-Json => {\"query\":\"a\"}") 19 | get "/post/level2/" do |_| 20 | " 21 | " 29 | end 30 | get "/post/level2" do |_| 31 | " 32 | " 40 | end 41 | post "/post/level2/" do |env| 42 | query = env.params.json["query"].as(String) 43 | "query: #{query}" 44 | end 45 | post "/post/level2" do |env| 46 | query = env.params.json["query"].as(String) 47 | "query: #{query}" 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /src/mazes/basic.cr: -------------------------------------------------------------------------------- 1 | def load_basic 2 | Xssmaze.push("basic-level1", "/basic/level1/?query=a", "no escape") 3 | get "/basic/level1/" do |env| 4 | env.params.query["query"] 5 | end 6 | get "/basic/level1" do |env| 7 | env.params.query["query"] 8 | end 9 | 10 | Xssmaze.push("basic-level2", "/basic/level2/?query=a", "escape to double-quot") 11 | get "/basic/level2/" do |env| 12 | env.params.query["query"].gsub("\"", """) 13 | end 14 | get "/basic/level2" do |env| 15 | env.params.query["query"].gsub("\"", """) 16 | end 17 | 18 | Xssmaze.push("basic-level3", "/basic/level3/?query=a", "escape to single-quot") 19 | get "/basic/level3/" do |env| 20 | env.params.query["query"].gsub("'", """) 21 | end 22 | get "/basic/level3" do |env| 23 | env.params.query["query"].gsub("'", """) 24 | end 25 | 26 | Xssmaze.push("basic-level4", "/basic/level4/?query=a", "escape to all quot") 27 | get "/basic/level4/" do |env| 28 | env.params.query["query"].gsub("\"", """).gsub("'", """) 29 | end 30 | get "/basic/level4" do |env| 31 | env.params.query["query"].gsub("\"", """).gsub("'", """) 32 | end 33 | 34 | Xssmaze.push("basic-level5", "/basic/level5/?query=a", "escape to parenthesis") 35 | get "/basic/level5/" do |env| 36 | env.params.query["query"].gsub("(", "").gsub(")", "") 37 | end 38 | get "/basic/level5" do |env| 39 | env.params.query["query"].gsub("(", "").gsub(")", "") 40 | end 41 | 42 | Xssmaze.push("basic-level6", "/basic/level6/?query=a", "escape to all quot and parenthesis") 43 | get "/basic/level6/" do |env| 44 | env.params.query["query"].gsub("\"", """).gsub("'", """).gsub("(", "").gsub(")", "") 45 | end 46 | get "/basic/level6" do |env| 47 | env.params.query["query"].gsub("\"", """).gsub("'", """).gsub("(", "").gsub(")", "") 48 | end 49 | 50 | Xssmaze.push("basic-level7", "/basic/level7/?query=a", "escape to all quot and parenthesis and backtick") 51 | get "/basic/level7/" do |env| 52 | env.params.query["query"].gsub("\"", """).gsub("'", """).gsub("(", "").gsub(")", "").gsub("`", "") 53 | end 54 | get "/basic/level7" do |env| 55 | env.params.query["query"].gsub("\"", """).gsub("'", """).gsub("(", "").gsub(")", "").gsub("`", "") 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /src/mazes/decode.cr: -------------------------------------------------------------------------------- 1 | require "base64" 2 | 3 | def load_decode 4 | Xssmaze.push("decode-level1", "/decode/level1/?query=a", "base64 decode") 5 | get "/decode/level1/" do |env| 6 | begin 7 | Base64.decode_string(env.params.query["query"]) 8 | rescue 9 | "Decode Error" 10 | end 11 | end 12 | get "/decode/level1" do |env| 13 | begin 14 | Base64.decode_string(env.params.query["query"]) 15 | rescue 16 | "Decode Error" 17 | end 18 | end 19 | 20 | Xssmaze.push("decode-level2", "/decode/level2/?query=a", "url decode") 21 | get "/decode/level2/" do |env| 22 | begin 23 | if env.params.query["query"].includes?("<") 24 | "Detect Special Charactor" 25 | else 26 | URI.decode(env.params.query["query"]) 27 | end 28 | rescue 29 | "Decode Error" 30 | end 31 | end 32 | get "/decode/level2" do |env| 33 | begin 34 | if env.params.query["query"].includes?("<") 35 | "Detect Special Charactor" 36 | else 37 | URI.decode(env.params.query["query"]) 38 | end 39 | rescue 40 | "Decode Error" 41 | end 42 | end 43 | 44 | Xssmaze.push("decode-level3", "/decode/level3/?query=a", "double url decode") 45 | get "/decode/level3/" do |env| 46 | begin 47 | data = URI.decode(env.params.query["query"]) 48 | if data.includes?("<") 49 | "Detect Special Charactor" 50 | else 51 | URI.decode(data) 52 | end 53 | rescue 54 | "Decode Error" 55 | end 56 | end 57 | get "/decode/level3" do |env| 58 | begin 59 | data = URI.decode(env.params.query["query"]) 60 | if data.includes?("<") 61 | "Detect Special Charactor" 62 | else 63 | URI.decode(data) 64 | end 65 | rescue 66 | "Decode Error" 67 | end 68 | end 69 | 70 | Xssmaze.push("decode-level4", "/decode/level4/?query=a", "double base64 decode") 71 | get "/decode/level4/" do |env| 72 | begin 73 | data = Base64.decode_string(env.params.query["query"]) 74 | Base64.decode_string(data) 75 | rescue 76 | "Decode Error" 77 | end 78 | end 79 | get "/decode/level4" do |env| 80 | begin 81 | data = Base64.decode_string(env.params.query["query"]) 82 | Base64.decode_string(data) 83 | rescue 84 | "Decode Error" 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Crystal CI](https://github.com/hahwul/xssmaze/actions/workflows/crystal_build.yml/badge.svg)](https://github.com/hahwul/xssmaze/actions/workflows/crystal_build.yml) 4 | [![Crystal Lint](https://github.com/hahwul/xssmaze/actions/workflows/crystal_lint.yml/badge.svg)](https://github.com/hahwul/xssmaze/actions/workflows/crystal_lint.yml) 5 | [![Docker](https://github.com/hahwul/xssmaze/actions/workflows/ghcr.yml/badge.svg)](https://github.com/hahwul/xssmaze/actions/workflows/ghcr.yml) 6 | 7 | XSSMaze is a web service configured to be vulnerable to XSS and is intended to measure and enhance the performance of security testing tools. You can find several vulnerable cases in the list below. 8 | 9 | ![](images/showcase.png) 10 | 11 | ## Installation 12 | ### From Source 13 | ```bash 14 | # Install dependencies 15 | shards install 16 | 17 | # Build 18 | shards build # Dev build 19 | shards build --release --no-debug --production 20 | 21 | # Run XSSMaze 22 | # Defatul: http://0.0.0.0:3000 23 | ./bin/xssmaze 24 | ``` 25 | 26 | ### From Docker 27 | ```bash 28 | docker pull ghcr.io/hahwul/xssmaze:main 29 | ``` 30 | 31 | ## Usage 32 | ```bash 33 | ./xssmaze 34 | 35 | # -b HOST, --bind HOST Host to bind (defaults to 0.0.0.0) 36 | # -p PORT, --port PORT Port to listen for connections (defaults to 3000) 37 | # -s, --ssl Enables SSL 38 | # --ssl-key-file FILE SSL key file 39 | # --ssl-cert-file FILE SSL certificate file 40 | # -h, --help Shows this help 41 | ``` 42 | 43 | ## Map API 44 | ``` 45 | curl http://localhost:3000/map/txt 46 | curl http://localhost:3000/map/json 47 | ``` 48 | 49 | ```http 50 | HTTP/1.1 200 OK 51 | Connection: keep-alive 52 | Content-Length: 611 53 | Content-Type: application/json 54 | X-Powered-By: Kemal 55 | 56 | { 57 | "endpoints": [ 58 | "/basic/level1/?query=a", 59 | "/basic/level2/?query=a", 60 | "/basic/level3/?query=a", 61 | "/basic/level4/?query=a", 62 | "/basic/level5/?query=a", 63 | "/basic/level6/?query=a", 64 | "/basic/level7/?query=a", 65 | "/dom/level1/", 66 | "/dom/level2/", 67 | "/dom/level3/", 68 | "/dom/level4/" 69 | ... 70 | ] 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /src/mazes/inattr_xss.cr: -------------------------------------------------------------------------------- 1 | def load_inattr_xss 2 | Xssmaze.push("inattr-xss-level1", "/inattr/level1/?query=a", "inattr-xss (double quote)") 3 | get "/inattr/level1/" do |env| 4 | query = env.params.query["query"] 5 | 6 | "
Hello
" 7 | end 8 | get "/inattr/level1" do |env| 9 | query = env.params.query["query"] 10 | 11 | "
Hello
" 12 | end 13 | 14 | Xssmaze.push("inattr-xss-level2", "/inattr/level2/?query=a", "inattr-xss (single quote)") 15 | get "/inattr/level2/" do |env| 16 | query = env.params.query["query"] 17 | 18 | "
Hello
" 19 | end 20 | get "/inattr/level2" do |env| 21 | query = env.params.query["query"] 22 | 23 | "
Hello
" 24 | end 25 | 26 | Xssmaze.push("inattr-xss-level3", "/inattr/level3/?query=a", "inattr-xss (double quote with <> fileter)") 27 | get "/inattr/level3/" do |env| 28 | query = env.params.query["query"] 29 | 30 | "
", "")}\">Hello
" 31 | end 32 | get "/inattr/level3" do |env| 33 | query = env.params.query["query"] 34 | 35 | "
", "")}\">Hello
" 36 | end 37 | 38 | Xssmaze.push("inattr-xss-level4", "/inattr/level4/?query=a", "inattr-xss (single quote with <> filter)") 39 | get "/inattr/level4/" do |env| 40 | query = env.params.query["query"] 41 | 42 | "
Hello
" 43 | end 44 | get "/inattr/level4" do |env| 45 | query = env.params.query["query"] 46 | 47 | "
Hello
" 48 | end 49 | 50 | Xssmaze.push("inattr-xss-level5", "/inattr/level5/?query=a", "inattr-xss (double quote with <> and blank filter)") 51 | get "/inattr/level5/" do |env| 52 | query = env.params.query["query"] 53 | 54 | "
", "").gsub(" ", "")}\">Hello
" 55 | end 56 | get "/inattr/level5" do |env| 57 | query = env.params.query["query"] 58 | 59 | "
", "").gsub(" ", "")}\">Hello
" 60 | end 61 | 62 | Xssmaze.push("inattr-xss-level6", "/inattr/level6/?query=a", "inattr-xss (single quote with <> and blank filter)") 63 | get "/inattr/level6/" do |env| 64 | query = env.params.query["query"] 65 | 66 | "
Hello
" 67 | end 68 | get "/inattr/level6" do |env| 69 | query = env.params.query["query"] 70 | 71 | "
Hello
" 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /src/mazes/injs_xss.cr: -------------------------------------------------------------------------------- 1 | def load_injs_xss 2 | Xssmaze.push("injs-xss-level1", "/injs/level1/?query=a", "injs-xss") 3 | get "/injs/level1/" do |env| 4 | query = env.params.query["query"] 5 | 6 | "" 9 | end 10 | get "/injs/level1" do |env| 11 | query = env.params.query["query"] 12 | 13 | "" 16 | end 17 | 18 | Xssmaze.push("injs-xss-level2", "/injs/level2/?query=a", "injs-xss - in single quote") 19 | get "/injs/level2/" do |env| 20 | query = env.params.query["query"] 21 | 22 | "" 25 | end 26 | get "/injs/level2" do |env| 27 | query = env.params.query["query"] 28 | 29 | "" 32 | end 33 | 34 | Xssmaze.push("injs-xss-level3", "/injs/level3/?query=a", "injs-xss - in double quote") 35 | get "/injs/level3/" do |env| 36 | query = env.params.query["query"] 37 | 38 | "" 41 | end 42 | get "/injs/level3" do |env| 43 | query = env.params.query["query"] 44 | 45 | "" 48 | end 49 | 50 | Xssmaze.push("injs-xss-level4", "/injs/level4/?query=a", "injs-xss - in single quote and double quote") 51 | get "/injs/level4/" do |env| 52 | query = env.params.query["query"].gsub("'", "") 53 | 54 | "" 57 | end 58 | get "/injs/level4" do |env| 59 | query = env.params.query["query"].gsub("'", "") 60 | 61 | "" 64 | end 65 | 66 | Xssmaze.push("injs-xss-level5", "/injs/level5/?query=a", "injs-xss - in comments style 1") 67 | get "/injs/level5/" do |env| 68 | query = env.params.query["query"] 69 | 70 | "" 73 | end 74 | get "/injs/level5" do |env| 75 | query = env.params.query["query"] 76 | 77 | "" 80 | end 81 | 82 | Xssmaze.push("injs-xss-level6", "/injs/level6/?query=a", "injs-xss - in comments style 2") 83 | get "/injs/level6/" do |env| 84 | query = env.params.query["query"] 85 | 86 | "" 89 | end 90 | get "/injs/level6" do |env| 91 | query = env.params.query["query"] 92 | 93 | "" 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /.github/workflows/ghcr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: GHCR Publish 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | on: 8 | push: 9 | branches: [main, dev] 10 | tags: [v*.*.*] 11 | release: 12 | types: [published] 13 | workflow_dispatch: 14 | 15 | env: 16 | # Use docker.io for Docker Hub if empty 17 | REGISTRY: ghcr.io 18 | # github.repository as / 19 | IMAGE_NAME: ${{ github.repository }} 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | packages: write 26 | # This is used to complete the identity challenge 27 | # with sigstore/fulcio when running outside of PRs. 28 | id-token: write 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | 33 | # Install the cosign tool except on PR 34 | # https://github.com/sigstore/cosign-installer 35 | - name: Install cosign 36 | if: github.event_name != 'pull_request' 37 | uses: sigstore/cosign-installer@v3.1.1 38 | with: 39 | cosign-release: v2.1.1 40 | 41 | # Using QEME for multiple platforms 42 | # https://github.com/docker/build-push-action?tab=readme-ov-file#usage 43 | - name: Set up QEMU 44 | uses: docker/setup-qemu-action@v3 45 | 46 | # Workaround: https://github.com/docker/build-push-action/issues/461 47 | - name: Setup Docker buildx 48 | uses: docker/setup-buildx-action@v3 49 | 50 | # Login against a Docker registry except on PR 51 | # https://github.com/docker/login-action 52 | - name: Log into registry ${{ env.REGISTRY }} 53 | if: github.event_name != 'pull_request' 54 | uses: docker/login-action@v3 55 | with: 56 | registry: ${{ env.REGISTRY }} 57 | username: ${{ github.actor }} 58 | password: ${{ secrets.GITHUB_TOKEN }} 59 | 60 | # Extract metadata (tags, labels) for Docker 61 | # https://github.com/docker/metadata-action 62 | - name: Extract Docker metadata 63 | id: meta 64 | uses: docker/metadata-action@v5 65 | with: 66 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 67 | 68 | # Build and push Docker image with Buildx (don't push on PR) 69 | # https://github.com/docker/build-push-action 70 | - name: Build and push Docker image 71 | id: build-and-push 72 | uses: docker/build-push-action@v5 73 | with: 74 | context: . 75 | push: true 76 | tags: ${{ steps.meta.outputs.tags }} 77 | labels: ${{ steps.meta.outputs.labels }} 78 | platforms: linux/amd64, linux/arm64 79 | cache-from: type=gha 80 | cache-to: type=gha,mode=max 81 | -------------------------------------------------------------------------------- /src/mazes/svg_xss.cr: -------------------------------------------------------------------------------- 1 | def load_svg_xss 2 | Xssmaze.push("svg-xss-level1", "/svg/level1/?query=a", "SVG onload XSS") 3 | get "/svg/level1/" do |env| 4 | query = env.params.query["query"] 5 | 6 | " 7 |

SVG XSS Level 1

8 | 9 | " 10 | end 11 | get "/svg/level1" do |env| 12 | query = env.params.query["query"] 13 | 14 | " 15 |

SVG XSS Level 1

16 | 17 | " 18 | end 19 | 20 | Xssmaze.push("svg-xss-level2", "/svg/level2/?query=a", "SVG animate XSS") 21 | get "/svg/level2/" do |env| 22 | query = env.params.query["query"] 23 | 24 | " 25 |

SVG XSS Level 2

26 | 27 | " 28 | end 29 | get "/svg/level2" do |env| 30 | query = env.params.query["query"] 31 | 32 | " 33 |

SVG XSS Level 2

34 | 35 | " 36 | end 37 | 38 | Xssmaze.push("svg-xss-level3", "/svg/level3/?query=a", "SVG foreignObject XSS") 39 | get "/svg/level3/" do |env| 40 | query = env.params.query["query"] 41 | 42 | " 43 |

SVG XSS Level 3

44 | 45 | " 46 | end 47 | get "/svg/level3" do |env| 48 | query = env.params.query["query"] 49 | 50 | " 51 |

SVG XSS Level 3

52 | 53 | " 54 | end 55 | 56 | Xssmaze.push("svg-xss-level4", "/svg/level4/?query=a", "SVG use XSS with href") 57 | get "/svg/level4/" do |env| 58 | query = env.params.query["query"] 59 | 60 | " 61 |

SVG XSS Level 4

62 | 63 | " 64 | end 65 | get "/svg/level4" do |env| 66 | query = env.params.query["query"] 67 | 68 | " 69 |

SVG XSS Level 4

70 | 71 | " 72 | end 73 | 74 | Xssmaze.push("svg-xss-level5", "/svg/level5/?query=a", "SVG embedded with data URI") 75 | get "/svg/level5/" do |env| 76 | query = env.params.query["query"] 77 | 78 | " 79 |

SVG XSS Level 5

80 | \"> 81 | " 82 | end 83 | get "/svg/level5" do |env| 84 | query = env.params.query["query"] 85 | 86 | " 87 |

SVG XSS Level 5

88 | \"> 89 | " 90 | end 91 | 92 | Xssmaze.push("svg-xss-level6", "/svg/level6/?query=a", "SVG with filtered script tags") 93 | get "/svg/level6/" do |env| 94 | query = env.params.query["query"].gsub("", "") 95 | 96 | " 97 |

SVG XSS Level 6

98 | 99 | " 100 | end 101 | get "/svg/level6" do |env| 102 | query = env.params.query["query"].gsub("", "") 103 | 104 | " 105 |

SVG XSS Level 6

106 | 107 | " 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /src/mazes/event_handler.cr: -------------------------------------------------------------------------------- 1 | def common_logic(query) 2 | query = query.gsub("<", "").gsub(">", "") 3 | "
Event Handler Test :: #{query}
" 4 | end 5 | 6 | def sanitize_query(query, level) 7 | case level 8 | when 2 9 | query = query.gsub(/on(error|load|click)/i, "") 10 | when 3 11 | query = query.gsub(/on(error|load|click|mouseover|focus|blur|keypress)/i, "") 12 | when 4 13 | query = query.gsub(/on(error|load|click|mouseover|focus|blur|keypress|animation(start|end|iteration)|drag(start|end|over|leave))/i, "") 14 | query = query.gsub(/javascript:/i, "") 15 | when 5 16 | query = query.gsub(/on(afterprint|afterscriptexecute|animation(cancel|end|iteration|start)|auxclick|before(copy|cut|input|unload)|blur|cancel|canplay|canplaythrough|change|click|close|contextmenu|copy|cuechange|cut|dblclick|drag(start|end|over|leave)|drop|durationchange|ended|error|focus(in|out)?|fullscreenchange|hashchange|input|invalid|keydown|keypress|keyup|load(start|ed(data|metadata)?)?|message|mousedown|mouseenter|mouseleave|mousemove|mouseout|mouseover|mouseup|pagehide|pageshow|paste|pause|play(ing)?|pointer(cancel|down|enter|leave|move|out|over|up)|progress|ratechange|reset|resize|scroll|seeked|seeking|select(start|end|ionchange)?|show|submit|suspend|timeupdate|toggle|touch(end|move|start)|transition(cancel|end|run|start)|unhandledrejection|unload|volumechange|waiting|wheel|webkit(animation(start|end|iteration)|mouse(force(willbegin|changed|down|up)|playbacktargetavailabilitychanged)|presentationmodechanged|fullscreenchange|willrevealbottom|transitionend))/i, "") 17 | end 18 | query 19 | end 20 | 21 | def load_eventhandler_xss 22 | Xssmaze.push("eventhandler-xss-level1", "/eventhandler/level1/?query=a", "eventhandler-xss (basic)") 23 | get "/eventhandler/level1/" do |env| 24 | query = env.params.query["query"] 25 | common_logic(query) 26 | end 27 | get "/eventhandler/level1" do |env| 28 | query = env.params.query["query"] 29 | common_logic(query) 30 | end 31 | 32 | Xssmaze.push("eventhandler-xss-level2", "/eventhandler/level2/?query=a", "eventhandler-xss (level 2)") 33 | get "/eventhandler/level2/" do |env| 34 | query = env.params.query["query"] 35 | query = sanitize_query(query, 2) 36 | common_logic(query) 37 | end 38 | get "/eventhandler/level2" do |env| 39 | query = env.params.query["query"] 40 | query = sanitize_query(query, 2) 41 | common_logic(query) 42 | end 43 | 44 | Xssmaze.push("eventhandler-xss-level3", "/eventhandler/level3/?query=a", "eventhandler-xss (level 3)") 45 | get "/eventhandler/level3/" do |env| 46 | query = env.params.query["query"] 47 | query = sanitize_query(query, 3) 48 | common_logic(query) 49 | end 50 | get "/eventhandler/level3" do |env| 51 | query = env.params.query["query"] 52 | query = sanitize_query(query, 3) 53 | common_logic(query) 54 | end 55 | 56 | Xssmaze.push("eventhandler-xss-level4", "/eventhandler/level4/?query=a", "eventhandler-xss (level 4)") 57 | get "/eventhandler/level4/" do |env| 58 | query = env.params.query["query"] 59 | query = sanitize_query(query, 4) 60 | common_logic(query) 61 | end 62 | get "/eventhandler/level4" do |env| 63 | query = env.params.query["query"] 64 | query = sanitize_query(query, 4) 65 | common_logic(query) 66 | end 67 | 68 | Xssmaze.push("eventhandler-xss-level5", "/eventhandler/level5/?query=a", "eventhandler-xss (level 5)") 69 | get "/eventhandler/level5/" do |env| 70 | query = env.params.query["query"] 71 | query = sanitize_query(query, 5) 72 | common_logic(query) 73 | end 74 | get "/eventhandler/level5" do |env| 75 | query = env.params.query["query"] 76 | query = sanitize_query(query, 5) 77 | common_logic(query) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /src/mazes/csp_bypass.cr: -------------------------------------------------------------------------------- 1 | def load_csp_bypass 2 | Xssmaze.push("csp-bypass-level1", "/csp/level1/?query=a", "CSP bypass with unsafe-inline") 3 | get "/csp/level1/" do |env| 4 | query = env.params.query["query"] 5 | env.response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'unsafe-inline'" 6 | 7 | " 8 |

CSP Level 1

9 |
User input: #{query}
10 | " 11 | end 12 | get "/csp/level1" do |env| 13 | query = env.params.query["query"] 14 | env.response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'unsafe-inline'" 15 | 16 | " 17 |

CSP Level 1

18 |
User input: #{query}
19 | " 20 | end 21 | 22 | Xssmaze.push("csp-bypass-level2", "/csp/level2/?query=a", "CSP bypass with nonce") 23 | get "/csp/level2/" do |env| 24 | query = env.params.query["query"] 25 | nonce = "abc123" 26 | env.response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'nonce-#{nonce}'" 27 | 28 | " 29 |

CSP Level 2

30 | 33 | " 34 | end 35 | get "/csp/level2" do |env| 36 | query = env.params.query["query"] 37 | nonce = "abc123" 38 | env.response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'nonce-#{nonce}'" 39 | 40 | " 41 |

CSP Level 2

42 | 45 | " 46 | end 47 | 48 | Xssmaze.push("csp-bypass-level3", "/csp/level3/?query=a", "CSP bypass with eval and unsafe-eval") 49 | get "/csp/level3/" do |env| 50 | query = env.params.query["query"] 51 | env.response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'unsafe-eval'" 52 | 53 | " 54 |

CSP Level 3

55 | 58 | " 59 | end 60 | get "/csp/level3" do |env| 61 | query = env.params.query["query"] 62 | env.response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'unsafe-eval'" 63 | 64 | " 65 |

CSP Level 3

66 | 69 | " 70 | end 71 | 72 | Xssmaze.push("csp-bypass-level4", "/csp/level4/?query=a", "CSP bypass with strict policy (data: URI)") 73 | get "/csp/level4/" do |env| 74 | query = env.params.query["query"] 75 | env.response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'" 76 | 77 | " 78 |

CSP Level 4

79 | 80 | 85 | " 86 | end 87 | get "/csp/level4" do |env| 88 | query = env.params.query["query"] 89 | env.response.headers["Content-Security-Policy"] = "default-src 'self'; script-src 'self'" 90 | 91 | " 92 |

CSP Level 4

93 | 94 | 99 | " 100 | end 101 | 102 | Xssmaze.push("csp-bypass-level5", "/csp/level5/?query=a", "CSP bypass with meta tag injection") 103 | get "/csp/level5/" do |env| 104 | query = env.params.query["query"] 105 | 106 | " 107 | 108 | 109 |

CSP Level 5

110 | #{query} 111 | " 112 | end 113 | get "/csp/level5" do |env| 114 | query = env.params.query["query"] 115 | 116 | " 117 | 118 | 119 |

CSP Level 5

120 | #{query} 121 | " 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /src/mazes/json_xss.cr: -------------------------------------------------------------------------------- 1 | def load_json_xss 2 | Xssmaze.push("json-xss-level1", "/json/level1/?query=a", "JSON response XSS (JSONP)") 3 | get "/json/level1/" do |env| 4 | query = env.params.query["query"] 5 | callback = env.params.query["callback"]? || "callback" 6 | env.response.content_type = "application/javascript" 7 | 8 | "#{callback}({\"message\": \"#{query}\", \"status\": \"success\"})" 9 | end 10 | get "/json/level1" do |env| 11 | query = env.params.query["query"] 12 | callback = env.params.query["callback"]? || "callback" 13 | env.response.content_type = "application/javascript" 14 | 15 | "#{callback}({\"message\": \"#{query}\", \"status\": \"success\"})" 16 | end 17 | 18 | Xssmaze.push("json-xss-level2", "/json/level2/?query=a", "JSON XSS with HTML entities bypass") 19 | get "/json/level2/" do |env| 20 | query = env.params.query["query"] 21 | env.response.content_type = "application/json" 22 | 23 | "{\"html_content\": \"
#{query}
\", \"escaped\": false}" 24 | end 25 | get "/json/level2" do |env| 26 | query = env.params.query["query"] 27 | env.response.content_type = "application/json" 28 | 29 | "{\"html_content\": \"
#{query}
\", \"escaped\": false}" 30 | end 31 | 32 | Xssmaze.push("json-xss-level3", "/json/level3/?query=a", "JSON XSS with Unicode escape") 33 | get "/json/level3/" do |env| 34 | query = env.params.query["query"] 35 | env.response.content_type = "application/json" 36 | 37 | # Simulate improper Unicode handling 38 | unicode_query = query.gsub("\\", "\\\\").gsub("\"", "\\\"") 39 | "{\"data\": \"#{unicode_query}\", \"type\": \"unicode\"}" 40 | end 41 | get "/json/level3" do |env| 42 | query = env.params.query["query"] 43 | env.response.content_type = "application/json" 44 | 45 | # Simulate improper Unicode handling 46 | unicode_query = query.gsub("\\", "\\\\").gsub("\"", "\\\"") 47 | "{\"data\": \"#{unicode_query}\", \"type\": \"unicode\"}" 48 | end 49 | 50 | Xssmaze.push("json-xss-level4", "/json/level4/?query=a", "JSON XSS in script tag context") 51 | get "/json/level4/" do |env| 52 | query = env.params.query["query"] 53 | 54 | " 55 |

JSON XSS Level 4

56 | 60 | " 61 | end 62 | get "/json/level4" do |env| 63 | query = env.params.query["query"] 64 | 65 | " 66 |

JSON XSS Level 4

67 | 71 | " 72 | end 73 | 74 | Xssmaze.push("json-xss-level5", "/json/level5/?query=a", "JSON XSS with array injection") 75 | get "/json/level5/" do |env| 76 | query = env.params.query["query"] 77 | 78 | " 79 |

JSON XSS Level 5

80 | 86 | " 87 | end 88 | get "/json/level5" do |env| 89 | query = env.params.query["query"] 90 | 91 | " 92 |

JSON XSS Level 5

93 | 99 | " 100 | end 101 | 102 | Xssmaze.push("json-xss-level6", "/json/level6/?query=a", "JSON XSS with nested object injection") 103 | get "/json/level6/" do |env| 104 | query = env.params.query["query"] 105 | 106 | " 107 |

JSON XSS Level 6

108 | 120 | " 121 | end 122 | get "/json/level6" do |env| 123 | query = env.params.query["query"] 124 | 125 | " 126 |

JSON XSS Level 6

127 | 139 | " 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /src/mazes/css_injection.cr: -------------------------------------------------------------------------------- 1 | def load_css_injection 2 | Xssmaze.push("css-injection-level1", "/css/level1/?query=a", "CSS expression() XSS (IE)") 3 | get "/css/level1/" do |env| 4 | query = env.params.query["query"] 5 | 6 | " 7 | 12 | 13 |

CSS Injection Level 1

14 |
Styled content
15 | " 16 | end 17 | get "/css/level1" do |env| 18 | query = env.params.query["query"] 19 | 20 | " 21 | 26 | 27 |

CSS Injection Level 1

28 |
Styled content
29 | " 30 | end 31 | 32 | Xssmaze.push("css-injection-level2", "/css/level2/?query=a", "CSS import with javascript: URL") 33 | get "/css/level2/" do |env| 34 | query = env.params.query["query"] 35 | 36 | " 37 | 40 | 41 |

CSS Injection Level 2

42 |
Import-based CSS injection
43 | " 44 | end 45 | get "/css/level2" do |env| 46 | query = env.params.query["query"] 47 | 48 | " 49 | 52 | 53 |

CSS Injection Level 2

54 |
Import-based CSS injection
55 | " 56 | end 57 | 58 | Xssmaze.push("css-injection-level3", "/css/level3/?query=a", "CSS background-image with javascript: URL") 59 | get "/css/level3/" do |env| 60 | query = env.params.query["query"] 61 | 62 | " 63 | 70 | 71 |

CSS Injection Level 3

72 |
73 | " 74 | end 75 | get "/css/level3" do |env| 76 | query = env.params.query["query"] 77 | 78 | " 79 | 86 | 87 |

CSS Injection Level 3

88 |
89 | " 90 | end 91 | 92 | Xssmaze.push("css-injection-level4", "/css/level4/?query=a", "CSS content property XSS") 93 | get "/css/level4/" do |env| 94 | query = env.params.query["query"] 95 | 96 | " 97 | 102 | 103 |

CSS Injection Level 4

104 |
Content injection
105 | " 106 | end 107 | get "/css/level4" do |env| 108 | query = env.params.query["query"] 109 | 110 | " 111 | 116 | 117 |

CSS Injection Level 4

118 |
Content injection
119 | " 120 | end 121 | 122 | Xssmaze.push("css-injection-level5", "/css/level5/?query=a", "CSS keyframes animation XSS") 123 | get "/css/level5/" do |env| 124 | query = env.params.query["query"] 125 | 126 | " 127 | 135 | 136 |

CSS Injection Level 5

137 |
Animated element
138 | " 139 | end 140 | get "/css/level5" do |env| 141 | query = env.params.query["query"] 142 | 143 | " 144 | 152 | 153 |

CSS Injection Level 5

154 |
Animated element
155 | " 156 | end 157 | 158 | Xssmaze.push("css-injection-level6", "/css/level6/?query=a", "CSS attr() function with HTML injection") 159 | get "/css/level6/" do |env| 160 | query = env.params.query["query"] 161 | 162 | " 163 | 168 | 169 |

CSS Injection Level 6

170 |
Element with attr content
171 | " 172 | end 173 | get "/css/level6" do |env| 174 | query = env.params.query["query"] 175 | 176 | " 177 | 182 | 183 |

CSS Injection Level 6

184 |
Element with attr content
185 | " 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /src/xssmaze.cr: -------------------------------------------------------------------------------- 1 | require "kemal" 2 | require "./maze" 3 | require "./mazes/**" 4 | require "./banner" 5 | 6 | module Xssmaze 7 | VERSION = "0.1.0" 8 | @@mazes = [] of Maze 9 | 10 | def self.push(name : String, url : String, desc : String) 11 | maze = Maze.new(name, url, desc) 12 | @@mazes << maze 13 | end 14 | 15 | def self.get 16 | @@mazes 17 | end 18 | end 19 | 20 | banner 21 | 22 | # Routes 23 | load_basic 24 | load_dom 25 | load_header 26 | load_path 27 | load_post 28 | load_redirect 29 | load_decode 30 | load_hidden_xss 31 | load_injs_xss 32 | load_inframe_xss 33 | load_inattr_xss 34 | load_jf_xss 35 | load_eventhandler_xss 36 | load_csp_bypass 37 | load_svg_xss 38 | load_css_injection 39 | load_template_injection 40 | load_websocket_xss 41 | load_json_xss 42 | load_advanced_xss 43 | 44 | # Index 45 | list = Xssmaze.get 46 | 47 | # Group mazes by type 48 | grouped_mazes = Hash(String, Array(Maze)).new 49 | 50 | list.each do |obj| 51 | # Extract type from name (e.g., "basic-level1" -> "basic") 52 | parts = obj.name.split("-") 53 | type = parts.size > 0 ? parts[0] : "other" 54 | grouped_mazes[type] ||= [] of Maze 55 | grouped_mazes[type] << obj 56 | end 57 | 58 | # Sort types alphabetically 59 | sorted_types = grouped_mazes.keys.sort! 60 | 61 | # Build hierarchical HTML 62 | indexdata = "
63 |
    " 64 | 65 | sorted_types.each do |type| 66 | indexdata += "
  • #{type}" 67 | 68 | mazes = grouped_mazes[type] 69 | indexdata += "
      " 70 | mazes.each do |maze| 71 | indexdata += "
    • #{maze.name} - #{maze.desc}
    • " 72 | end 73 | indexdata += "
    " 74 | 75 | indexdata += "
  • " 76 | end 77 | 78 | indexdata += "
" 79 | 80 | get "/" do 81 | " 82 | 83 | 84 | 85 | 86 | XSSMaze 87 | 217 | 218 | 219 |
220 |

XSSMaze

221 |

XSSMaze is a web service configured to be vulnerable to XSS and is intended to measure and enhance the performance of security testing tools.

222 |

All vulnerable parameters are named query.

223 |

You can find several vulnerable cases in the list below.

224 | 229 |
230 | #{indexdata} 231 | 232 | " 233 | end 234 | 235 | get "/map/text" do |env| 236 | env.response.content_type = "text/plain" 237 | tmp = "" 238 | list.each do |obj| 239 | tmp += "#{obj.url}\n" 240 | end 241 | 242 | tmp 243 | end 244 | 245 | get "/map/json" do |env| 246 | env.response.content_type = "application/json" 247 | tmp = "{\"endpoints\": [" 248 | list.each do |obj| 249 | tmp += "\"#{obj.url}\"," 250 | end 251 | tmp = tmp[0...-1] + "]}" 252 | 253 | tmp 254 | end 255 | 256 | Kemal.run 257 | -------------------------------------------------------------------------------- /src/mazes/template_injection.cr: -------------------------------------------------------------------------------- 1 | def load_template_injection 2 | Xssmaze.push("template-injection-level1", "/template/level1/?query=a", "Server-side template injection (basic)") 3 | get "/template/level1/" do |env| 4 | query = env.params.query["query"] 5 | template = "Hello {{user_input}}" 6 | output = template.gsub("{{user_input}}", query) 7 | 8 | " 9 |

Template Injection Level 1

10 |
#{output}
11 | " 12 | end 13 | get "/template/level1" do |env| 14 | query = env.params.query["query"] 15 | template = "Hello {{user_input}}" 16 | output = template.gsub("{{user_input}}", query) 17 | 18 | " 19 |

Template Injection Level 1

20 |
#{output}
21 | " 22 | end 23 | 24 | Xssmaze.push("template-injection-level2", "/template/level2/?query=a", "Client-side template injection (Handlebars style)") 25 | get "/template/level2/" do |env| 26 | query = env.params.query["query"] 27 | 28 | " 29 |

Template Injection Level 2

30 |
31 | 38 | " 39 | end 40 | get "/template/level2" do |env| 41 | query = env.params.query["query"] 42 | 43 | " 44 |

Template Injection Level 2

45 |
46 | 53 | " 54 | end 55 | 56 | Xssmaze.push("template-injection-level3", "/template/level3/?query=a", "Template injection with expression evaluation") 57 | get "/template/level3/" do |env| 58 | query = env.params.query["query"] 59 | 60 | " 61 |

Template Injection Level 3

62 |
63 | 73 | " 74 | end 75 | get "/template/level3" do |env| 76 | query = env.params.query["query"] 77 | 78 | " 79 |

Template Injection Level 3

80 |
81 | 91 | " 92 | end 93 | 94 | Xssmaze.push("template-injection-level4", "/template/level4/?query=a", "Template injection with conditional rendering") 95 | get "/template/level4/" do |env| 96 | query = env.params.query["query"] 97 | 98 | " 99 |

Template Injection Level 4

100 |
101 | 106 | " 107 | end 108 | get "/template/level4" do |env| 109 | query = env.params.query["query"] 110 | 111 | " 112 |

Template Injection Level 4

113 |
114 | 119 | " 120 | end 121 | 122 | Xssmaze.push("template-injection-level5", "/template/level5/?query=a", "Template injection with loop rendering") 123 | get "/template/level5/" do |env| 124 | query = env.params.query["query"] 125 | 126 | " 127 |

Template Injection Level 5

128 |
129 | 137 | " 138 | end 139 | get "/template/level5" do |env| 140 | query = env.params.query["query"] 141 | 142 | " 143 |

Template Injection Level 5

144 |
145 | 153 | " 154 | end 155 | 156 | Xssmaze.push("template-injection-level6", "/template/level6/?query=a", "Template injection with sanitization bypass") 157 | get "/template/level6/" do |env| 158 | query = env.params.query["query"].gsub("", "</script>") 159 | 160 | " 161 |

Template Injection Level 6

162 |
163 | 168 | " 169 | end 170 | get "/template/level6" do |env| 171 | query = env.params.query["query"].gsub("", "</script>") 172 | 173 | " 174 |

Template Injection Level 6

175 |
176 | 181 | " 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /src/mazes/websocket_xss.cr: -------------------------------------------------------------------------------- 1 | def load_websocket_xss 2 | Xssmaze.push("websocket-xss-level1", "/websocket/level1/?query=a", "WebSocket message XSS (basic)") 3 | get "/websocket/level1/" do |env| 4 | query = env.params.query["query"] 5 | 6 | " 7 |

WebSocket XSS Level 1

8 |
9 | 19 | " 20 | end 21 | get "/websocket/level1" do |env| 22 | query = env.params.query["query"] 23 | 24 | " 25 |

WebSocket XSS Level 1

26 |
27 | 37 | " 38 | end 39 | 40 | Xssmaze.push("websocket-xss-level2", "/websocket/level2/?query=a", "WebSocket JSON message XSS") 41 | get "/websocket/level2/" do |env| 42 | query = env.params.query["query"] 43 | 44 | " 45 |

WebSocket XSS Level 2

46 |
47 | 58 | " 59 | end 60 | get "/websocket/level2" do |env| 61 | query = env.params.query["query"] 62 | 63 | " 64 |

WebSocket XSS Level 2

65 |
66 | 77 | " 78 | end 79 | 80 | Xssmaze.push("websocket-xss-level3", "/websocket/level3/?query=a", "WebSocket with HTML message rendering") 81 | get "/websocket/level3/" do |env| 82 | query = env.params.query["query"] 83 | 84 | " 85 |

WebSocket XSS Level 3

86 |
87 | 104 | " 105 | end 106 | get "/websocket/level3" do |env| 107 | query = env.params.query["query"] 108 | 109 | " 110 |

WebSocket XSS Level 3

111 |
112 | 129 | " 130 | end 131 | 132 | Xssmaze.push("websocket-xss-level4", "/websocket/level4/?query=a", "WebSocket with eval-based message processing") 133 | get "/websocket/level4/" do |env| 134 | query = env.params.query["query"] 135 | 136 | " 137 |

WebSocket XSS Level 4

138 |
139 | 153 | " 154 | end 155 | get "/websocket/level4" do |env| 156 | query = env.params.query["query"] 157 | 158 | " 159 |

WebSocket XSS Level 4

160 |
161 | 175 | " 176 | end 177 | 178 | Xssmaze.push("websocket-xss-level5", "/websocket/level5/?query=a", "WebSocket with DOM manipulation") 179 | get "/websocket/level5/" do |env| 180 | query = env.params.query["query"] 181 | 182 | " 183 |

WebSocket XSS Level 5

184 |
185 | 201 | " 202 | end 203 | get "/websocket/level5" do |env| 204 | query = env.params.query["query"] 205 | 206 | " 207 |

WebSocket XSS Level 5

208 |
209 | 225 | " 226 | end 227 | end 228 | -------------------------------------------------------------------------------- /src/mazes/advanced_xss.cr: -------------------------------------------------------------------------------- 1 | def load_advanced_xss 2 | Xssmaze.push("advanced-xss-level1", "/advanced/level1/?query=a", "XSS with WAF bypass using encoding") 3 | get "/advanced/level1/" do |env| 4 | query = env.params.query["query"] 5 | # Simulate basic WAF filtering 6 | filtered_query = query.gsub("script", "").gsub("javascript", "").gsub("onload", "") 7 | 8 | " 9 |

Advanced XSS Level 1

10 |
Filtered input: #{filtered_query}
11 | " 12 | end 13 | get "/advanced/level1" do |env| 14 | query = env.params.query["query"] 15 | # Simulate basic WAF filtering 16 | filtered_query = query.gsub("script", "").gsub("javascript", "").gsub("onload", "") 17 | 18 | " 19 |

Advanced XSS Level 1

20 |
Filtered input: #{filtered_query}
21 | " 22 | end 23 | 24 | Xssmaze.push("advanced-xss-level2", "/advanced/level2/?query=a", "XSS with mutation observer") 25 | get "/advanced/level2/" do |env| 26 | query = env.params.query["query"] 27 | 28 | " 29 |

Advanced XSS Level 2

30 |
31 | 53 | " 54 | end 55 | get "/advanced/level2" do |env| 56 | query = env.params.query["query"] 57 | 58 | " 59 |

Advanced XSS Level 2

60 |
61 | 83 | " 84 | end 85 | 86 | Xssmaze.push("advanced-xss-level3", "/advanced/level3/?query=a", "XSS with Service Worker") 87 | get "/advanced/level3/" do |env| 88 | query = env.params.query["query"] 89 | 90 | " 91 |

Advanced XSS Level 3

92 |
93 | 105 | " 106 | end 107 | get "/advanced/level3" do |env| 108 | query = env.params.query["query"] 109 | 110 | " 111 |

Advanced XSS Level 3

112 |
113 | 125 | " 126 | end 127 | 128 | Xssmaze.push("advanced-xss-level4", "/advanced/level4/?query=a", "XSS with Web Components") 129 | get "/advanced/level4/" do |env| 130 | query = env.params.query["query"] 131 | 132 | " 133 |

Advanced XSS Level 4

134 |
135 | 149 | " 150 | end 151 | get "/advanced/level4" do |env| 152 | query = env.params.query["query"] 153 | 154 | " 155 |

Advanced XSS Level 4

156 |
157 | 171 | " 172 | end 173 | 174 | Xssmaze.push("advanced-xss-level5", "/advanced/level5/?query=a", "XSS with Trusted Types bypass") 175 | get "/advanced/level5/" do |env| 176 | query = env.params.query["query"] 177 | 178 | " 179 |

Advanced XSS Level 5

180 |
181 | 201 | " 202 | end 203 | get "/advanced/level5" do |env| 204 | query = env.params.query["query"] 205 | 206 | " 207 |

Advanced XSS Level 5

208 |
209 | 229 | " 230 | end 231 | 232 | Xssmaze.push("advanced-xss-level6", "/advanced/level6/?query=a", "XSS with Proxy object manipulation") 233 | get "/advanced/level6/" do |env| 234 | query = env.params.query["query"] 235 | 236 | " 237 |

Advanced XSS Level 6

238 |
239 | 257 | " 258 | end 259 | get "/advanced/level6" do |env| 260 | query = env.params.query["query"] 261 | 262 | " 263 |

Advanced XSS Level 6

264 |
265 | 283 | " 284 | end 285 | end 286 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # XSSMaze Development Instructions 2 | 3 | XSSMaze is a Crystal-based web application designed to be vulnerable to XSS (Cross-Site Scripting) attacks. It serves as a testing platform to measure and enhance the performance of security testing tools. The application uses the Kemal web framework and provides various XSS vulnerability scenarios. 4 | 5 | Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. 6 | 7 | ## Working Effectively 8 | 9 | ### Prerequisites and Setup 10 | - Install Crystal programming language (version 1.8.2 as specified in shard.yml) 11 | - Ensure network connectivity for dependency installation 12 | - Docker (optional, for containerized builds) 13 | 14 | ### Bootstrap, Build, and Test the Repository 15 | ```bash 16 | # Install Crystal dependencies 17 | shards install 18 | # NEVER CANCEL: Dependency installation can take 5-10 minutes depending on network speed. Set timeout to 15+ minutes. 19 | 20 | # Development build 21 | shards build 22 | # NEVER CANCEL: Build takes 2-5 minutes depending on system. Set timeout to 10+ minutes. 23 | 24 | # Production build (recommended for deployment) 25 | shards build --release --no-debug --production 26 | # NEVER CANCEL: Production build takes 3-7 minutes with optimizations. Set timeout to 15+ minutes. 27 | 28 | # Run tests 29 | crystal spec 30 | # NEVER CANCEL: Test suite takes 1-2 minutes. Set timeout to 5+ minutes. 31 | ``` 32 | 33 | ### Running the Application 34 | ```bash 35 | # Run XSSMaze (after successful build) 36 | ./bin/xssmaze 37 | 38 | # Alternative: Run with custom configuration 39 | ./bin/xssmaze -b 127.0.0.1 -p 8080 40 | 41 | # Default runs on http://0.0.0.0:3000 42 | ``` 43 | 44 | ### Command Line Options 45 | - `-b HOST, --bind HOST`: Host to bind (defaults to 0.0.0.0) 46 | - `-p PORT, --port PORT`: Port to listen for connections (defaults to 3000) 47 | - `-s, --ssl`: Enables SSL 48 | - `--ssl-key-file FILE`: SSL key file 49 | - `--ssl-cert-file FILE`: SSL certificate file 50 | - `-h, --help`: Shows help 51 | 52 | ### Docker Build and Run 53 | ```bash 54 | # Build Docker image 55 | docker build -t xssmaze . 56 | # NEVER CANCEL: Docker build takes 10-15 minutes. Set timeout to 30+ minutes. 57 | 58 | # Run using pre-built image 59 | docker run -p 3000:3000 ghcr.io/hahwul/xssmaze:main 60 | 61 | # Build and run for ARM 62 | docker build -f Dockerfile.arm -t xssmaze-arm . 63 | ``` 64 | 65 | ## Validation 66 | 67 | ### Manual Testing Scenarios 68 | After building and running XSSMaze, ALWAYS validate functionality by: 69 | 70 | 1. **Basic Connectivity Test:** 71 | ```bash 72 | curl http://localhost:3000/ 73 | # Should return HTML with XSSMaze title and endpoint list 74 | ``` 75 | 76 | 2. **API Endpoint Tests:** 77 | ```bash 78 | # Test map endpoints 79 | curl http://localhost:3000/map/text 80 | curl http://localhost:3000/map/json 81 | # Should return list of vulnerable endpoints 82 | ``` 83 | 84 | 3. **XSS Vulnerability Tests:** 85 | ```bash 86 | # Test basic XSS endpoint 87 | curl "http://localhost:3000/basic/level1/?query=" 88 | # Should return the script tag (demonstrates vulnerability) 89 | 90 | # Test escaped XSS endpoint 91 | curl "http://localhost:3000/basic/level2/?query=" 92 | # Should return escaped content 93 | ``` 94 | 95 | 4. **Complete User Scenario:** 96 | - Access the main page at http://localhost:3000 97 | - Verify all XSS test cases are listed 98 | - Test at least 3 different vulnerability levels 99 | - Check that /map/json returns valid JSON with endpoint list 100 | 101 | ### Linting and Code Quality 102 | ```bash 103 | # Install Crystal Ameba linter (if not installed) 104 | # The CI uses crystal-ameba/github-action@v0.8.0 105 | 106 | # Run linting (via GitHub Actions workflow) 107 | # Manual linting requires Ameba to be installed separately 108 | ``` 109 | 110 | ## Repository Structure and Navigation 111 | 112 | ### Key Directories 113 | ``` 114 | /home/runner/work/xssmaze/xssmaze/ 115 | ├── src/ # Main source code 116 | │ ├── xssmaze.cr # Application entry point 117 | │ ├── maze.cr # Maze class definition 118 | │ ├── banner.cr # Application banner 119 | │ └── mazes/ # XSS vulnerability implementations 120 | │ ├── basic.cr # Basic XSS scenarios 121 | │ ├── dom.cr # DOM-based XSS 122 | │ ├── header.cr # Header injection XSS 123 | │ ├── post.cr # POST-based XSS 124 | │ └── [others].cr # Additional XSS types 125 | ├── spec/ # Test files 126 | │ ├── spec_helper.cr # Test configuration 127 | │ └── xssmaze_spec.cr # Main test suite (minimal) 128 | ├── .github/workflows/ # CI/CD configuration 129 | ├── shard.yml # Dependencies (like package.json) 130 | ├── shard.lock # Lock file with exact versions 131 | ├── Dockerfile # Docker build configuration 132 | └── README.md # Project documentation 133 | ``` 134 | 135 | ### Important Files to Review When Making Changes 136 | - Always check `src/xssmaze.cr` after modifying route definitions 137 | - Review `shard.yml` when adding new dependencies 138 | - Update `README.md` if adding new XSS vulnerability types 139 | - Check existing maze files in `src/mazes/` for patterns when adding new vulnerabilities 140 | 141 | ### XSS Maze Categories 142 | The application includes these vulnerability categories: 143 | - **Basic XSS** (`basic.cr`): 7 levels of basic reflection vulnerabilities 144 | - **DOM XSS** (`dom.cr`): Client-side DOM manipulation vulnerabilities 145 | - **Header Injection** (`header.cr`): HTTP header-based XSS 146 | - **Path-based** (`path.cr`): URL path injection vulnerabilities 147 | - **POST-based** (`post.cr`): Form submission XSS 148 | - **Redirect** (`redirect.cr`): URL redirection vulnerabilities 149 | - **Decode** (`decode.cr`): Encoding/decoding bypass scenarios 150 | - **Hidden XSS** (`hidden_xss.cr`): Non-obvious XSS cases 151 | - **Injection XSS** (`injs_xss.cr`): SQL injection combined with XSS 152 | - **In-Frame XSS** (`inframe_xss.cr`): Frame-based XSS 153 | - **In-Attribute XSS** (`inattr_xss.cr`): HTML attribute injection 154 | - **JavaScript Function XSS** (`jf_xss.cr`): Function call vulnerabilities 155 | - **Event Handler XSS** (`event_handler.cr`): HTML event handler injection 156 | 157 | ## Common Tasks and Troubleshooting 158 | 159 | ### Dependency Issues 160 | - If `shards install` fails due to network issues, check internet connectivity 161 | - Dependency resolution can fail if GitHub is inaccessible 162 | - The application requires the Kemal framework (version 1.7.2) 163 | 164 | ### Build Failures 165 | - Ensure Crystal version matches shard.yml specification (1.8.2) 166 | - Build failures often relate to syntax errors in .cr files 167 | - Production builds are more strict than development builds 168 | 169 | ### Runtime Issues 170 | - Application runs on port 3000 by default 171 | - Check if port is available: `lsof -i :3000` 172 | - Application requires no external database or services 173 | 174 | ### CI/CD Information 175 | - GitHub Actions run Crystal builds and linting automatically 176 | - Build workflow uses `crystallang/crystal` Docker image 177 | - Lint workflow uses `crystal-ameba/github-action@v0.8.0` 178 | - Docker images are published to `ghcr.io/hahwul/xssmaze:main` 179 | 180 | ## Timing Expectations and Timeouts 181 | 182 | Based on successful GitHub Actions builds, here are realistic timing expectations: 183 | 184 | | Operation | Expected Time | Timeout Recommendation | Notes | 185 | |-----------|---------------|------------------------|-------| 186 | | `shards install` | 1-30 seconds | 5+ minutes | Network dependent | 187 | | `shards build` | 15-30 seconds | 5+ minutes | Development build | 188 | | `shards build --release` | 30-90 seconds | 5+ minutes | Production optimized | 189 | | `crystal spec` | 5-15 seconds | 2+ minutes | Minimal test suite | 190 | | Docker build | 5-15 minutes | 30+ minutes | Multi-stage build with downloads | 191 | | Application startup | 1-3 seconds | 30 seconds | Port binding time | 192 | 193 | **CRITICAL**: NEVER CANCEL any build or test commands. Crystal compilation can be slow, especially for release builds with optimizations. 194 | 195 | **Note**: Times above are based on CI environment performance. Local builds may vary significantly based on system specs and network connectivity. 196 | 197 | ## Network Dependencies 198 | 199 | The application requires internet access for: 200 | - Installing dependencies via `shards install` 201 | - Fetching Kemal framework and related libraries from GitHub 202 | - Docker base image downloads 203 | 204 | If network access is restricted: 205 | - Build may fail with "Could not resolve host" errors 206 | - Use pre-built Docker images when available: `docker pull ghcr.io/hahwul/xssmaze:main` 207 | - Consider offline Crystal installation methods 208 | 209 | ### Network Troubleshooting 210 | - Docker build failures with "fatal: unable to access" indicate network connectivity issues 211 | - Pre-built images may have compatibility issues in some environments 212 | - If Docker image fails with library errors, the environment may lack required system libraries (libpcre2, libgc, etc.) 213 | 214 | ## Development Patterns 215 | 216 | ### Adding New XSS Scenarios 217 | When adding new vulnerability scenarios, follow these patterns: 218 | 219 | 1. **Create route in maze file:** 220 | ```crystal 221 | Xssmaze.push("scenario-name", "/path/to/endpoint?query=a", "description") 222 | get "/path/to/endpoint" do |env| 223 | # XSS vulnerability implementation 224 | env.params.query["query"] # or manipulated version 225 | end 226 | ``` 227 | 228 | 2. **Load maze in main file:** 229 | Add `load_your_maze` call in `src/xssmaze.cr` 230 | 231 | 3. **Test the endpoint:** 232 | ```bash 233 | curl "http://localhost:3000/your/endpoint?query=" 234 | ``` 235 | 236 | ### Crystal Language Patterns 237 | - Use `env.params.query["parameter"]` to access GET parameters 238 | - Use `env.params.body["parameter"]` for POST parameters 239 | - String manipulation: `.gsub("old", "new")` for replacements 240 | - HTML escaping methods available through Kemal framework -------------------------------------------------------------------------------- /src/mazes/dom.cr: -------------------------------------------------------------------------------- 1 | def load_dom 2 | Xssmaze.push("dom-level1", "/dom/level1/", "dom write (location.href)") 3 | get "/dom/level1/" do |_| 4 | "" 7 | end 8 | get "/dom/level1" do |_| 9 | "" 12 | end 13 | 14 | Xssmaze.push("dom-level2", "/dom/level2/", "dom write (location.hash)") 15 | get "/dom/level2/" do |_| 16 | "" 19 | end 20 | get "/dom/level2" do |_| 21 | "" 24 | end 25 | 26 | Xssmaze.push("dom-level3", "/dom/level3/", "redirect (location.hash)") 27 | get "/dom/level3/" do |_| 28 | "" 31 | end 32 | get "/dom/level3" do |_| 33 | "" 36 | end 37 | 38 | Xssmaze.push("dom-level4", "/dom/level4/", "dom write (query param)") 39 | get "/dom/level4/" do |_| 40 | "" 45 | end 46 | get "/dom/level4" do |_| 47 | "" 52 | end 53 | 54 | Xssmaze.push("dom-level5", "/dom/level5/", "dom write (query param)") 55 | get "/dom/level5/" do |_| 56 | "" 61 | end 62 | get "/dom/level5" do |_| 63 | "" 68 | end 69 | 70 | Xssmaze.push("dom-level6", "/dom/level6/", "location.href (query param)") 71 | get "/dom/level6/" do |_| 72 | "" 77 | end 78 | get "/dom/level6" do |_| 79 | "" 84 | end 85 | 86 | # Simple innerHTML manipulation 87 | Xssmaze.push("dom-level7", "/dom/level7/", "innerHTML (location.hash)") 88 | get "/dom/level7/" do |_| 89 | "
90 | " 93 | end 94 | get "/dom/level7" do |_| 95 | "
96 | " 99 | end 100 | 101 | Xssmaze.push("dom-level8", "/dom/level8/", "innerHTML (query param)") 102 | get "/dom/level8/" do |_| 103 | "
104 | " 109 | end 110 | get "/dom/level8" do |_| 111 | "
112 | " 117 | end 118 | 119 | # innerText manipulation (safer but can be misused) 120 | Xssmaze.push("dom-level9", "/dom/level9/", "innerText to script tag") 121 | get "/dom/level9/" do |_| 122 | " 123 | " 128 | end 129 | get "/dom/level9" do |_| 130 | " 131 | " 136 | end 137 | 138 | # Element attribute manipulation - src 139 | Xssmaze.push("dom-level10", "/dom/level10/", "img src (query param)") 140 | get "/dom/level10/" do |_| 141 | " 142 | " 147 | end 148 | get "/dom/level10" do |_| 149 | " 150 | " 155 | end 156 | 157 | # Element attribute manipulation - href 158 | Xssmaze.push("dom-level11", "/dom/level11/", "anchor href (location.hash)") 159 | get "/dom/level11/" do |_| 160 | "Click here 161 | " 164 | end 165 | get "/dom/level11" do |_| 166 | "Click here 167 | " 170 | end 171 | 172 | # document.cookie reflection 173 | Xssmaze.push("dom-level12", "/dom/level12/", "document.write (document.cookie)") 174 | get "/dom/level12/" do |_| 175 | "" 178 | end 179 | get "/dom/level12" do |_| 180 | "" 183 | end 184 | 185 | # window.name reflection 186 | Xssmaze.push("dom-level13", "/dom/level13/", "innerHTML (window.name)") 187 | get "/dom/level13/" do |_| 188 | "
189 | " 192 | end 193 | get "/dom/level13" do |_| 194 | "
195 | " 198 | end 199 | 200 | # document.referrer reflection 201 | Xssmaze.push("dom-level14", "/dom/level14/", "document.write (document.referrer)") 202 | get "/dom/level14/" do |_| 203 | "" 206 | end 207 | get "/dom/level14" do |_| 208 | "" 211 | end 212 | 213 | # insertAdjacentHTML 214 | Xssmaze.push("dom-level15", "/dom/level15/", "insertAdjacentHTML (query param)") 215 | get "/dom/level15/" do |_| 216 | "
217 | " 222 | end 223 | get "/dom/level15" do |_| 224 | "
225 | " 230 | end 231 | 232 | # outerHTML manipulation 233 | Xssmaze.push("dom-level16", "/dom/level16/", "outerHTML (location.hash)") 234 | get "/dom/level16/" do |_| 235 | "
Original content
236 | " 239 | end 240 | get "/dom/level16" do |_| 241 | "
Original content
242 | " 245 | end 246 | 247 | # createElement with setAttribute 248 | Xssmaze.push("dom-level17", "/dom/level17/", "setAttribute href (query param)") 249 | get "/dom/level17/" do |_| 250 | "
251 | " 259 | end 260 | get "/dom/level17" do |_| 261 | "
262 | " 270 | end 271 | 272 | # iframe src manipulation 273 | Xssmaze.push("dom-level18", "/dom/level18/", "iframe src (location.hash)") 274 | get "/dom/level18/" do |_| 275 | " 276 | " 279 | end 280 | get "/dom/level18" do |_| 281 | " 282 | " 285 | end 286 | 287 | # setTimeout with string 288 | Xssmaze.push("dom-level19", "/dom/level19/", "setTimeout (query param)") 289 | get "/dom/level19/" do |_| 290 | "" 295 | end 296 | get "/dom/level19" do |_| 297 | "" 302 | end 303 | 304 | # setInterval with string 305 | Xssmaze.push("dom-level20", "/dom/level20/", "setInterval (location.hash)") 306 | get "/dom/level20/" do |_| 307 | "" 311 | end 312 | get "/dom/level20" do |_| 313 | "" 317 | end 318 | 319 | # Function constructor 320 | Xssmaze.push("dom-level21", "/dom/level21/", "Function constructor (query param)") 321 | get "/dom/level21/" do |_| 322 | "" 328 | end 329 | get "/dom/level21" do |_| 330 | "" 336 | end 337 | 338 | # JSON.parse with innerHTML 339 | Xssmaze.push("dom-level22", "/dom/level22/", "JSON.parse + innerHTML (query param)") 340 | get "/dom/level22/" do |_| 341 | "
342 | " 352 | end 353 | get "/dom/level22" do |_| 354 | "
355 | " 365 | end 366 | 367 | # postMessage receiver 368 | Xssmaze.push("dom-level23", "/dom/level23/", "postMessage + innerHTML") 369 | get "/dom/level23/" do |_| 370 | "
371 | " 376 | end 377 | get "/dom/level23" do |_| 378 | "
379 | " 384 | end 385 | 386 | # Template literal in eval-like context 387 | Xssmaze.push("dom-level24", "/dom/level24/", "template literal eval (query param)") 388 | get "/dom/level24/" do |_| 389 | "" 394 | end 395 | get "/dom/level24" do |_| 396 | "" 401 | end 402 | 403 | # DOM clobbering - name attribute 404 | Xssmaze.push("dom-level25", "/dom/level25/", "DOM clobbering (query param)") 405 | get "/dom/level25/" do |_| 406 | "
407 | " 415 | end 416 | get "/dom/level25" do |_| 417 | "
418 | " 426 | end 427 | 428 | # document.URL reflection 429 | Xssmaze.push("dom-level26", "/dom/level26/", "document.write (document.URL)") 430 | get "/dom/level26/" do |_| 431 | "" 434 | end 435 | get "/dom/level26" do |_| 436 | "" 439 | end 440 | 441 | # location.search reflection 442 | Xssmaze.push("dom-level27", "/dom/level27/", "innerHTML (location.search)") 443 | get "/dom/level27/" do |_| 444 | "
445 | " 448 | end 449 | get "/dom/level27" do |_| 450 | "
451 | " 454 | end 455 | 456 | # location.pathname reflection 457 | Xssmaze.push("dom-level28", "/dom/level28/", "document.write (location.pathname)") 458 | get "/dom/level28/" do |_| 459 | "" 462 | end 463 | get "/dom/level28" do |_| 464 | "" 467 | end 468 | 469 | # Anchor element with javascript: protocol 470 | Xssmaze.push("dom-level29", "/dom/level29/", "anchor click with javascript: (query param)") 471 | get "/dom/level29/" do |_| 472 | "Click me 473 | " 478 | end 479 | get "/dom/level29" do |_| 480 | "Click me 481 | " 486 | end 487 | 488 | # document.execCommand (legacy but still works in some contexts) 489 | Xssmaze.push("dom-level30", "/dom/level30/", "insertHTML execCommand (query param)") 490 | get "/dom/level30/" do |_| 491 | "
492 | " 498 | end 499 | get "/dom/level30" do |_| 500 | "
501 | " 507 | end 508 | 509 | # Range.createContextualFragment 510 | Xssmaze.push("dom-level31", "/dom/level31/", "createContextualFragment (location.hash)") 511 | get "/dom/level31/" do |_| 512 | "
513 | " 518 | end 519 | get "/dom/level31" do |_| 520 | "
521 | " 526 | end 527 | 528 | # DOMParser 529 | Xssmaze.push("dom-level32", "/dom/level32/", "DOMParser (query param)") 530 | get "/dom/level32/" do |_| 531 | "
532 | " 539 | end 540 | get "/dom/level32" do |_| 541 | "
542 | " 549 | end 550 | 551 | # Multiple URL parameters concatenated 552 | Xssmaze.push("dom-level33", "/dom/level33/", "multiple params concatenation") 553 | get "/dom/level33/" do |_| 554 | "
555 | " 561 | end 562 | get "/dom/level33" do |_| 563 | "
564 | " 570 | end 571 | 572 | # Object property access 573 | Xssmaze.push("dom-level34", "/dom/level34/", "object property innerHTML (query param)") 574 | get "/dom/level34/" do |_| 575 | "
576 | " 582 | end 583 | get "/dom/level34" do |_| 584 | "
585 | " 591 | end 592 | 593 | # Array join 594 | Xssmaze.push("dom-level35", "/dom/level35/", "array join innerHTML (location.hash)") 595 | get "/dom/level35/" do |_| 596 | "
597 | " 601 | end 602 | get "/dom/level35" do |_| 603 | "
604 | " 608 | end 609 | end 610 | --------------------------------------------------------------------------------