security: validate revision parameter to prevent Nix injection

The revision parameter was interpolated directly into a Nix expression,
allowing potential injection of arbitrary Nix code. An attacker could
craft a revision string like:
  "; builtins.readFile /etc/passwd; "

This adds ValidateRevision() which ensures revisions only contain safe
characters (alphanumeric, hyphens, underscores, dots) and are at most
64 characters long.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 19:10:31 +01:00
parent 5f0445e749
commit be1ff4839b
2 changed files with 76 additions and 0 deletions

View File

@@ -12,6 +12,57 @@ import (
// TestNixpkgsRevision is the revision from flake.lock used for testing.
const TestNixpkgsRevision = "e6eae2ee2110f3d31110d5c222cd395303343b08"
// TestValidateRevision tests the revision validation function.
func TestValidateRevision(t *testing.T) {
tests := []struct {
name string
revision string
wantErr bool
}{
// Valid cases
{"valid git hash", "e6eae2ee2110f3d31110d5c222cd395303343b08", false},
{"valid short hash", "e6eae2e", false},
{"valid channel name", "nixos-unstable", false},
{"valid channel with version", "nixos-24.11", false},
{"valid underscore", "some_branch", false},
{"valid mixed", "release-24.05_beta", false},
// Invalid cases - injection attempts
{"injection semicolon", "foo; rm -rf /", true},
{"injection quotes", `"; builtins.readFile /etc/passwd; "`, true},
{"injection backticks", "foo`whoami`", true},
{"injection dollar", "foo$(whoami)", true},
{"injection newline", "foo\nbar", true},
{"injection space", "foo bar", true},
{"injection slash", "foo/bar", true},
{"injection backslash", "foo\\bar", true},
{"injection pipe", "foo|bar", true},
{"injection ampersand", "foo&bar", true},
{"injection redirect", "foo>bar", true},
{"injection less than", "foo<bar", true},
{"injection curly braces", "foo{bar}", true},
{"injection parens", "foo(bar)", true},
{"injection brackets", "foo[bar]", true},
// Edge cases
{"empty string", "", true},
{"too long", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true},
{"just dots", "...", false}, // dots are allowed, path traversal is handled elsewhere
{"single char", "a", false},
{"max length 64", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false},
{"65 chars", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateRevision(tt.revision)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateRevision(%q) error = %v, wantErr %v", tt.revision, err, tt.wantErr)
}
})
}
}
// BenchmarkIndexRevision benchmarks indexing a full nixpkgs revision.
// This is a slow benchmark that requires nix to be installed.
// Run with: go test -bench=BenchmarkIndexRevision -benchtime=1x -timeout=30m ./internal/nixos/...