mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-05 08:18:17 +00:00
Compare commits
3 Commits
v1.23.0-pr
...
Xe/analyti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9ef5f651b | ||
|
|
7ed1753fcc | ||
|
|
3dab060bfa |
2
.github/workflows/docker-pr.yml
vendored
2
.github/workflows/docker-pr.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
|
||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
brew bundle
|
||||
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
2
.github/workflows/docs-deploy.yml
vendored
2
.github/workflows/docs-deploy.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: techarohq
|
||||
|
||||
6
.github/workflows/go.yml
vendored
6
.github/workflows/go.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
brew bundle
|
||||
|
||||
- name: Setup Golang caches
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
${{ runner.os }}-golang-
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
|
||||
4
.github/workflows/package-builds-stable.yml
vendored
4
.github/workflows/package-builds-stable.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
brew bundle
|
||||
|
||||
- name: Setup Golang caches
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Setup Homebrew cellar cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
/home/linuxbrew/.linuxbrew/Cellar
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
brew bundle
|
||||
|
||||
- name: Setup Golang caches
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
run: |
|
||||
go tool yeet
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: packages
|
||||
path: var/*
|
||||
|
||||
4
.github/workflows/smoke-tests.yml
vendored
4
.github/workflows/smoke-tests.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
run: echo "ARTIFACT_NAME=${{ matrix.test }}" | sed 's|/|-|g' >> $GITHUB_ENV
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
if: always()
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
|
||||
2
.github/workflows/ssh-ci-runner-cron.yml
vendored
2
.github/workflows/ssh-ci-runner-cron.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
4
.github/workflows/zizmor.yml
vendored
4
.github/workflows/zizmor.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload SARIF file
|
||||
uses: github/codeql-action/upload-sarif@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
|
||||
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
@@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Allow multiple consecutive slashes in a row in application paths ([#754](https://github.com/TecharoHQ/anubis/issues/754)).
|
||||
- Add option to set `targetSNI` to special keyword 'auto' to indicate that it should be automatically set to the request Host name ([424](https://github.com/TecharoHQ/anubis/issues/424)).
|
||||
- The Preact challenge has been removed from the default configuration. It will be deprecated in the future.
|
||||
- An open redirect when in subrequest mode has been fixed.
|
||||
|
||||
### Potentially breaking changes
|
||||
|
||||
|
||||
2
go.mod
2
go.mod
@@ -3,7 +3,7 @@ module github.com/TecharoHQ/anubis
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/TecharoHQ/thoth-proto v0.4.0
|
||||
github.com/TecharoHQ/thoth-proto v0.5.0
|
||||
github.com/a-h/templ v0.3.924
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6
|
||||
|
||||
2
go.sum
2
go.sum
@@ -37,6 +37,8 @@ github.com/Songmu/gitconfig v0.2.1 h1:cZsqELfMtxWVI8ovq17gbvsR4qLfoYLAiXy5GwtJWb
|
||||
github.com/Songmu/gitconfig v0.2.1/go.mod h1:XM4O3SoXFnli9Ql2G7qXK2Fg7LJwf7Hs8GLFEOJlzmM=
|
||||
github.com/TecharoHQ/thoth-proto v0.4.0 h1:UbkvfgCku0Dm1R6O4ug3HOsJNnE6F3wB8x+Dpw2lzFI=
|
||||
github.com/TecharoHQ/thoth-proto v0.4.0/go.mod h1:IcGnZt3iYUZQVEa0Lwk5l4ix0hCeXlWUV1TJMZvbWx0=
|
||||
github.com/TecharoHQ/thoth-proto v0.5.0 h1:Fa663s4soYiURSU8MfW9tZ2wF+LsCRSaYmjUSyagfBM=
|
||||
github.com/TecharoHQ/thoth-proto v0.5.0/go.mod h1:C/U7FqTxpVn4V/qebC/GcW32I0h9xzsmWehF27KFOJs=
|
||||
github.com/TecharoHQ/yeet v0.6.3 h1:Iev6TYt/tpFYU73kbkNIYjCObYTvlihtby+htGF4Us8=
|
||||
github.com/TecharoHQ/yeet v0.6.3/go.mod h1:ltt+PWPjnvmQJxEHsdJ5K9u3GoWK83vSLWCCp8XbxqI=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||
|
||||
31
lib/http.go
31
lib/http.go
@@ -296,6 +296,16 @@ func (s *Server) constructRedirectURL(r *http.Request) (string, error) {
|
||||
if proto == "" || host == "" || uri == "" {
|
||||
return "", errors.New(localizer.T("missing_required_forwarded_headers"))
|
||||
}
|
||||
|
||||
switch proto {
|
||||
case "http", "https":
|
||||
// allowed
|
||||
default:
|
||||
lg := internal.GetRequestLogger(s.logger, r)
|
||||
lg.Warn("invalid protocol in X-Forwarded-Proto", "proto", proto)
|
||||
return "", errors.New(localizer.T("invalid_redirect"))
|
||||
}
|
||||
|
||||
// Check if host is allowed in RedirectDomains (supports '*' via glob)
|
||||
if len(s.opts.RedirectDomains) > 0 && !matchRedirectDomain(s.opts.RedirectDomains, host) {
|
||||
lg := internal.GetRequestLogger(s.logger, r)
|
||||
@@ -369,16 +379,31 @@ func (s *Server) ServeHTTPNext(w http.ResponseWriter, r *http.Request) {
|
||||
localizer := localization.GetLocalizer(r)
|
||||
|
||||
redir := r.FormValue("redir")
|
||||
urlParsed, err := r.URL.Parse(redir)
|
||||
urlParsed, err := url.ParseRequestURI(redir)
|
||||
if err != nil {
|
||||
s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), makeCode(err), http.StatusBadRequest)
|
||||
// if ParseRequestURI fails, try as relative URL
|
||||
urlParsed, err = r.URL.Parse(redir)
|
||||
if err != nil {
|
||||
s.respondWithStatus(w, r, localizer.T("redirect_not_parseable"), makeCode(err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// validate URL scheme to prevent javascript:, data:, file:, tel:, etc.
|
||||
switch urlParsed.Scheme {
|
||||
case "", "http", "https":
|
||||
// allowed: empty scheme means relative URL
|
||||
default:
|
||||
lg := internal.GetRequestLogger(s.logger, r)
|
||||
lg.Warn("XSS attempt blocked, invalid redirect scheme", "scheme", urlParsed.Scheme, "redir", redir)
|
||||
s.respondWithStatus(w, r, localizer.T("invalid_redirect"), "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
hostNotAllowed := len(urlParsed.Host) > 0 &&
|
||||
len(s.opts.RedirectDomains) != 0 &&
|
||||
!matchRedirectDomain(s.opts.RedirectDomains, urlParsed.Host)
|
||||
hostMismatch := r.URL.Host != "" && urlParsed.Host != r.URL.Host
|
||||
hostMismatch := r.URL.Host != "" && urlParsed.Host != "" && urlParsed.Host != r.URL.Host
|
||||
|
||||
if hostNotAllowed || hostMismatch {
|
||||
lg := internal.GetRequestLogger(s.logger, r)
|
||||
|
||||
296
lib/redirect_security_test.go
Normal file
296
lib/redirect_security_test.go
Normal file
@@ -0,0 +1,296 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/TecharoHQ/anubis/lib/policy"
|
||||
)
|
||||
|
||||
func TestRedirectSecurity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
testType string // "constructRedirectURL", "serveHTTPNext", "renderIndex"
|
||||
|
||||
// For constructRedirectURL tests
|
||||
xForwardedProto string
|
||||
xForwardedHost string
|
||||
xForwardedUri string
|
||||
|
||||
// For serveHTTPNext tests
|
||||
redirParam string
|
||||
reqHost string
|
||||
|
||||
// For renderIndex tests
|
||||
returnHTTPStatusOnly bool
|
||||
|
||||
// Expected results
|
||||
expectedStatus int
|
||||
shouldError bool
|
||||
shouldNotRedirect bool
|
||||
shouldBlock bool
|
||||
errorContains string
|
||||
}{
|
||||
// constructRedirectURL tests - X-Forwarded-Proto validation
|
||||
{
|
||||
name: "constructRedirectURL: javascript protocol should be rejected",
|
||||
testType: "constructRedirectURL",
|
||||
xForwardedProto: "javascript",
|
||||
xForwardedHost: "example.com",
|
||||
xForwardedUri: "alert(1)",
|
||||
shouldError: true,
|
||||
errorContains: "invalid",
|
||||
},
|
||||
{
|
||||
name: "constructRedirectURL: data protocol should be rejected",
|
||||
testType: "constructRedirectURL",
|
||||
xForwardedProto: "data",
|
||||
xForwardedHost: "text/html",
|
||||
xForwardedUri: ",<script>alert(1)</script>",
|
||||
shouldError: true,
|
||||
errorContains: "invalid",
|
||||
},
|
||||
{
|
||||
name: "constructRedirectURL: file protocol should be rejected",
|
||||
testType: "constructRedirectURL",
|
||||
xForwardedProto: "file",
|
||||
xForwardedHost: "",
|
||||
xForwardedUri: "/etc/passwd",
|
||||
shouldError: true,
|
||||
errorContains: "invalid",
|
||||
},
|
||||
{
|
||||
name: "constructRedirectURL: ftp protocol should be rejected",
|
||||
testType: "constructRedirectURL",
|
||||
xForwardedProto: "ftp",
|
||||
xForwardedHost: "example.com",
|
||||
xForwardedUri: "/file.txt",
|
||||
shouldError: true,
|
||||
errorContains: "invalid",
|
||||
},
|
||||
{
|
||||
name: "constructRedirectURL: https protocol should be allowed",
|
||||
testType: "constructRedirectURL",
|
||||
xForwardedProto: "https",
|
||||
xForwardedHost: "example.com",
|
||||
xForwardedUri: "/foo",
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "constructRedirectURL: http protocol should be allowed",
|
||||
testType: "constructRedirectURL",
|
||||
xForwardedProto: "http",
|
||||
xForwardedHost: "example.com",
|
||||
xForwardedUri: "/bar",
|
||||
shouldError: false,
|
||||
},
|
||||
|
||||
// serveHTTPNext tests - redir parameter validation
|
||||
{
|
||||
name: "serveHTTPNext: javascript: URL should be rejected",
|
||||
testType: "serveHTTPNext",
|
||||
redirParam: "javascript:alert(1)",
|
||||
reqHost: "example.com",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
shouldNotRedirect: true,
|
||||
},
|
||||
{
|
||||
name: "serveHTTPNext: data: URL should be rejected",
|
||||
testType: "serveHTTPNext",
|
||||
redirParam: "data:text/html,<script>alert(1)</script>",
|
||||
reqHost: "example.com",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
shouldNotRedirect: true,
|
||||
},
|
||||
{
|
||||
name: "serveHTTPNext: file: URL should be rejected",
|
||||
testType: "serveHTTPNext",
|
||||
redirParam: "file:///etc/passwd",
|
||||
reqHost: "example.com",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
shouldNotRedirect: true,
|
||||
},
|
||||
{
|
||||
name: "serveHTTPNext: vbscript: URL should be rejected",
|
||||
testType: "serveHTTPNext",
|
||||
redirParam: "vbscript:msgbox(1)",
|
||||
reqHost: "example.com",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
shouldNotRedirect: true,
|
||||
},
|
||||
{
|
||||
name: "serveHTTPNext: valid https URL should work",
|
||||
testType: "serveHTTPNext",
|
||||
redirParam: "https://example.com/foo",
|
||||
reqHost: "example.com",
|
||||
expectedStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
name: "serveHTTPNext: valid relative URL should work",
|
||||
testType: "serveHTTPNext",
|
||||
redirParam: "/foo/bar",
|
||||
reqHost: "example.com",
|
||||
expectedStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
name: "serveHTTPNext: external domain should be blocked",
|
||||
testType: "serveHTTPNext",
|
||||
redirParam: "https://evil.com/phishing",
|
||||
reqHost: "example.com",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
shouldBlock: true,
|
||||
},
|
||||
{
|
||||
name: "serveHTTPNext: relative path should work",
|
||||
testType: "serveHTTPNext",
|
||||
redirParam: "/safe/path",
|
||||
reqHost: "example.com",
|
||||
expectedStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
name: "serveHTTPNext: empty redir should show success page",
|
||||
testType: "serveHTTPNext",
|
||||
redirParam: "",
|
||||
reqHost: "example.com",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
|
||||
// renderIndex tests - full subrequest auth flow
|
||||
{
|
||||
name: "renderIndex: javascript protocol in X-Forwarded-Proto",
|
||||
testType: "renderIndex",
|
||||
xForwardedProto: "javascript",
|
||||
xForwardedHost: "example.com",
|
||||
xForwardedUri: "alert(1)",
|
||||
returnHTTPStatusOnly: true,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "renderIndex: data protocol in X-Forwarded-Proto",
|
||||
testType: "renderIndex",
|
||||
xForwardedProto: "data",
|
||||
xForwardedHost: "example.com",
|
||||
xForwardedUri: "text/html,<script>alert(1)</script>",
|
||||
returnHTTPStatusOnly: true,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "renderIndex: valid https redirect",
|
||||
testType: "renderIndex",
|
||||
xForwardedProto: "https",
|
||||
xForwardedHost: "example.com",
|
||||
xForwardedUri: "/protected/page",
|
||||
returnHTTPStatusOnly: true,
|
||||
expectedStatus: http.StatusTemporaryRedirect,
|
||||
},
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
opts: Options{
|
||||
PublicUrl: "https://anubis.example.com",
|
||||
RedirectDomains: []string{},
|
||||
},
|
||||
logger: slog.Default(),
|
||||
policy: &policy.ParsedConfig{},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
switch tt.testType {
|
||||
case "constructRedirectURL":
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", tt.xForwardedProto)
|
||||
req.Header.Set("X-Forwarded-Host", tt.xForwardedHost)
|
||||
req.Header.Set("X-Forwarded-Uri", tt.xForwardedUri)
|
||||
|
||||
redirectURL, err := s.constructRedirectURL(req)
|
||||
|
||||
if tt.shouldError {
|
||||
if err == nil {
|
||||
t.Errorf("expected error containing %q, got nil", tt.errorContains)
|
||||
t.Logf("got redirect URL: %s", redirectURL)
|
||||
} else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
|
||||
t.Logf("expected error containing %q, got: %v", tt.errorContains, err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("expected no error, got: %v", err)
|
||||
}
|
||||
// Verify the redirect URL is safe
|
||||
if redirectURL != "" {
|
||||
parsed, err := url.Parse(redirectURL)
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse redirect URL: %v", err)
|
||||
}
|
||||
redirParam := parsed.Query().Get("redir")
|
||||
if redirParam != "" {
|
||||
redirParsed, err := url.Parse(redirParam)
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse redir parameter: %v", err)
|
||||
}
|
||||
if redirParsed.Scheme != "http" && redirParsed.Scheme != "https" {
|
||||
t.Errorf("redir parameter has unsafe scheme: %s", redirParsed.Scheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "serveHTTPNext":
|
||||
req := httptest.NewRequest("GET", "/.within.website/?redir="+url.QueryEscape(tt.redirParam), nil)
|
||||
req.Host = tt.reqHost
|
||||
req.URL.Host = tt.reqHost
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
s.ServeHTTPNext(rr, req)
|
||||
|
||||
if rr.Code != tt.expectedStatus {
|
||||
t.Errorf("expected status %d, got %d", tt.expectedStatus, rr.Code)
|
||||
t.Logf("body: %s", rr.Body.String())
|
||||
}
|
||||
|
||||
if tt.shouldNotRedirect {
|
||||
location := rr.Header().Get("Location")
|
||||
if location != "" {
|
||||
t.Errorf("expected no redirect, but got Location header: %s", location)
|
||||
}
|
||||
}
|
||||
|
||||
if tt.shouldBlock {
|
||||
location := rr.Header().Get("Location")
|
||||
if location != "" && strings.Contains(location, "evil.com") {
|
||||
t.Errorf("redirect to evil.com was not blocked: %s", location)
|
||||
}
|
||||
}
|
||||
|
||||
case "renderIndex":
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", tt.xForwardedProto)
|
||||
req.Header.Set("X-Forwarded-Host", tt.xForwardedHost)
|
||||
req.Header.Set("X-Forwarded-Uri", tt.xForwardedUri)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
s.RenderIndex(rr, req, policy.CheckResult{}, nil, tt.returnHTTPStatusOnly)
|
||||
|
||||
if rr.Code != tt.expectedStatus {
|
||||
t.Errorf("expected status %d, got %d", tt.expectedStatus, rr.Code)
|
||||
}
|
||||
|
||||
if tt.expectedStatus == http.StatusTemporaryRedirect {
|
||||
location := rr.Header().Get("Location")
|
||||
if location == "" {
|
||||
t.Error("expected Location header, got none")
|
||||
} else {
|
||||
// Verify the location doesn't contain javascript:
|
||||
if strings.Contains(location, "javascript") {
|
||||
t.Errorf("Location header contains 'javascript': %s", location)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,39 +12,37 @@ spec:
|
||||
app: nginx-external-auth
|
||||
spec:
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: nginx-cfg
|
||||
containers:
|
||||
- name: www
|
||||
image: nginx:alpine
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
ports:
|
||||
- containerPort: 80
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/nginx/conf.d
|
||||
readOnly: true
|
||||
- name: anubis
|
||||
image: ttl.sh/techaro/anubis-external-auth:latest
|
||||
imagePullPolicy: Always
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 128Mi
|
||||
env:
|
||||
- name: TARGET
|
||||
value: " "
|
||||
- name: REDIRECT_DOMAINS
|
||||
value: nginx.local.cetacean.club
|
||||
|
||||
|
||||
configMap:
|
||||
name: nginx-cfg
|
||||
containers:
|
||||
- name: www
|
||||
image: nginx:alpine
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
ports:
|
||||
- containerPort: 80
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/nginx/conf.d
|
||||
readOnly: true
|
||||
- name: anubis
|
||||
image: ttl.sh/techaro/anubis:latest
|
||||
imagePullPolicy: Always
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 128Mi
|
||||
env:
|
||||
- name: TARGET
|
||||
value: " "
|
||||
- name: REDIRECT_DOMAINS
|
||||
value: nginx.local.cetacean.club
|
||||
|
||||
@@ -9,17 +9,17 @@ metadata:
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- nginx.local.cetacean.club
|
||||
secretName: nginx-local-cetacean-club-public-tls
|
||||
- hosts:
|
||||
- nginx.local.cetacean.club
|
||||
secretName: nginx-local-cetacean-club-public-tls
|
||||
rules:
|
||||
- host: nginx.local.cetacean.club
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: nginx-external-auth
|
||||
port:
|
||||
name: http
|
||||
- host: nginx.local.cetacean.club
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: nginx-external-auth
|
||||
port:
|
||||
name: http
|
||||
|
||||
@@ -7,4 +7,4 @@ configMapGenerator:
|
||||
- name: nginx-cfg
|
||||
behavior: create
|
||||
files:
|
||||
- ./conf.d/default.conf
|
||||
- ./conf.d/default.conf
|
||||
|
||||
@@ -6,8 +6,8 @@ spec:
|
||||
selector:
|
||||
app: nginx-external-auth
|
||||
ports:
|
||||
- name: http
|
||||
protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
- name: http
|
||||
protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
type: ClusterIP
|
||||
|
||||
@@ -4,20 +4,20 @@ set -euo pipefail
|
||||
|
||||
# Build container image
|
||||
(
|
||||
cd ../.. \
|
||||
&& npm ci \
|
||||
&& npm run container -- \
|
||||
--docker-repo ttl.sh/techaro/anubis-external-auth \
|
||||
--docker-tags ttl.sh/techaro/anubis-external-auth:latest
|
||||
cd ../.. &&
|
||||
npm ci &&
|
||||
npm run container -- \
|
||||
--docker-repo ttl.sh/techaro/anubis \
|
||||
--docker-tags ttl.sh/techaro/anubis:latest
|
||||
)
|
||||
|
||||
kubectl apply -k .
|
||||
echo "open https://nginx.local.cetacean.club, press control c when done"
|
||||
|
||||
control_c() {
|
||||
kubectl delete -k .
|
||||
exit
|
||||
kubectl delete -k .
|
||||
exit
|
||||
}
|
||||
trap control_c SIGINT
|
||||
|
||||
sleep infinity
|
||||
sleep infinity
|
||||
|
||||
Reference in New Issue
Block a user