module Xssmaze::Beacon

Overview

Execution oracle.

Every scoring path in this lab measures reflection: a scanner asks whether its string came back in the HTML and calls that a finding. That is a proxy, and a poor one — it scores a harmlessly-escaped echo as a hit, and it is blind to the ~160 dom endpoints where the payload never reaches the server response at all. The beacon replaces the proxy with proof: a payload has to actually run to reach /beacon/<token>, so a hit in the log is a true positive that no amount of string matching can fake.

/basic/level1/?query=<img src=/beacon/run1 onerror=fetch('/beacon/run1')> then: GET /beacon/log?token=run1

The recorded Referer is the payoff — it names the maze page that executed, so a headless harness can fire one token for a whole sweep and still attribute every hit to the endpoint that produced it.

Routes: ANY /beacon/ record a fire, answer with a 1x1 GIF GET /beacon/log the whole log GET /beacon/log?token=t one token DELETE /beacon/log clear it, so consecutive runs are isolated POST /beacon/log/clear the same reset for clients that cannot DELETE

This is instrumentation, not a maze, and it is deliberately boring: it never calls Xssmaze.push (the catalog is a benchmark denominator and must not grow a non-maze entry), it answers only image/gif and application/json, and nothing it records is ever echoed into an HTML response.

Defined in:

mazes/beacon.cr

Constant Summary

GIF_1X1 = Bytes[71, 73, 70, 56, 57, 97, 1, 0, 1, 0, 128, 0, 0, 0, 0, 0, 255, 255, 255, 33, 249, 4, 1, 0, 0, 0, 0, 44, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 2, 68, 1, 0, 59]

The canonical 43-byte transparent 1x1 GIF: the smallest thing an <img src=...> can load without drawing anything on the page under test.

MAX_HITS = 100
MAX_TOKENS = 1000

A fuzzer pointing at /beacon/<random> must not be able to exhaust memory, so the log is bounded on all three axes it can grow along: distinct tokens, stored hits per token, and the length kept from each attacker-set header.

MAX_VALUE = 256
RESERVED_TOKENS = ["log"] of ::String

/beacon/log is the log API for every verb, so a token by that name could never be read back. Refusing it up front beats letting a benchmark discover the collision from a permanently empty result.

TOKEN_PATTERN = /\A[A-Za-z0-9_-]{1,64}\z/

Class Method Summary

Class Method Detail

def self.clear #

[View source]
def self.headers(env) : Nil #

no-store because a cached beacon is a silently lost signal, and the wildcard CORS header because the payload firing it may well be running on another origin. nosniff keeps a browser from ever second-guessing the JSON content type and parsing a recorded Referer as markup.


[View source]
def self.hit(env) : String #

[View source]
def self.json(env) : Nil #

[View source]
def self.log_json #

[View source]
def self.preflight(env) : String #

[View source]
def self.record(token : String, method : String, referer : String | Nil, user_agent : String | Nil) : Bool #

Returns false only when the token cap refused a brand new token. Tokens already in the log keep recording, so a benchmark run in flight is never cut off by someone else fuzzing the beacon.


[View source]
def self.reject(env) : String #

[View source]
def self.token_json(token : String) #

[View source]
def self.valid_token?(token : String) : Bool #

[View source]