feat(modules): add laravel telescope dashboard exposure module (#279)

Detect an exposed Laravel Telescope dashboard, which reveals requests, queries,
exceptions and dumped variables. Telescope aborts with 403 before render when
gated, so a 200 carrying both the Telescope title and the Vue mount id proves
the gate is open. The and-condition on the mount id keeps an unrelated page that
merely names telescope, or a soft-404, from matching.
This commit is contained in:
Tigah
2026-07-22 12:36:10 -07:00
committed by GitHub
parent 1f3708531d
commit 7742b8e731
2 changed files with 90 additions and 0 deletions
@@ -0,0 +1,53 @@
package modules_test
import "testing"
// laravel-telescope-exposure fires only when the dashboard actually renders: a
// 200 carrying both the Telescope title and the Vue mount id. A 403-gated
// install, a soft-404 that reflects the word, and an unrelated "telescope" page
// must all stay silent.
func TestLaravelTelescopeExposureModule(t *testing.T) {
const telescope = "../../modules/recon/laravel-telescope-exposure.yaml"
dashboard := `<!DOCTYPE html><html><head><title>Telescope - Acme</title></head>` +
`<body><div id="telescope" v-cloak></div></body></html>`
t.Run("an exposed dashboard is flagged and the app name extracted", func(t *testing.T) {
res := runDebugModule(t, telescope, 200, dashboard)
if len(res.Findings) == 0 {
t.Fatal("expected a telescope finding")
}
if v := debugExtract(res, "app_name"); v != "Acme" {
t.Errorf("app_name=%q, want Acme", v)
}
})
t.Run("a dashboard without a configured app name still fires", func(t *testing.T) {
body := `<!DOCTYPE html><html><head><title>Telescope</title></head>` +
`<body><div id="telescope" v-cloak></div></body></html>`
if res := runDebugModule(t, telescope, 200, body); len(res.Findings) == 0 {
t.Fatal("expected a telescope finding without an app name")
}
})
t.Run("a gated install returning 403 is silent", func(t *testing.T) {
if res := runDebugModule(t, telescope, 403, dashboard); len(res.Findings) > 0 {
t.Errorf("403-gated telescope should not match, got %d findings", len(res.Findings))
}
})
t.Run("an unrelated telescope page without the vue mount is silent", func(t *testing.T) {
body := `<html><head><title>Telescope Reviews 2026</title></head>` +
`<body><ul id="telescope-list"></ul></body></html>`
if res := runDebugModule(t, telescope, 200, body); len(res.Findings) > 0 {
t.Errorf("astronomy page should not match, got %d findings", len(res.Findings))
}
})
t.Run("a generic soft-404 shell is silent", func(t *testing.T) {
body := `<html><head><title>404 Not Found</title></head><body>not found</body></html>`
if res := runDebugModule(t, telescope, 200, body); len(res.Findings) > 0 {
t.Errorf("soft-404 should not match, got %d findings", len(res.Findings))
}
})
}
@@ -0,0 +1,37 @@
# Laravel Telescope Dashboard Exposure Detection Module
id: laravel-telescope-exposure
info:
name: Laravel Telescope Dashboard Exposure
author: sif
severity: high
description: Detects an exposed Laravel Telescope debug dashboard that reveals requests, queries, exceptions, and dumped variables
tags: [laravel, telescope, debug, exposure, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/telescope"
- "{{BaseURL}}/telescope/requests"
matchers:
- type: status
status:
- 200
- type: word
part: body
words:
- '<title>Telescope'
- 'id="telescope" v-cloak'
condition: and
extractors:
- type: regex
name: app_name
part: body
regex:
- '<title>Telescope - ([^<]+)</title>'
group: 1