diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-17 19:30:43 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-17 19:30:43 +0200 |
| commit | 04450e10866fc56890426525062fd5cdf56c4300 (patch) | |
| tree | 952fb91c85aa9e03f9b7d3de93ee5af42b30324d /test | |
| parent | 5221c5400e193a64ab607d7de7c0450f5f15cd55 (diff) | |
Allowlist aggregate order columns and template names
Page.aggregate interpolated order_by into SQL unchecked while
already allowlisting order_direction; the column is now normalized
and checked against the sortable columns, falling back to pages.id.
Its values arrive from editor-authored aggregate shortcodes, so
this was editor-gated, but the asymmetry was wrong regardless.
template_name and default_template_name now validate inclusion in
Page.custom_templates -- names render as filesystem paths, so only
names actually present in the template directory are acceptable.
Validated only on change: legacy rows whose template file has since
vanished stay saveable, and valid_template's render-time fallback
to standard_template continues to cover them. Two tests that wrote
fabricated template names through the front door now arrange their
state correctly (update_column for the stale-name fallback test, a
real template for the update-persists test).
Diffstat (limited to 'test')
| -rw-r--r-- | test/controllers/content_controller_test.rb | 3 | ||||
| -rw-r--r-- | test/controllers/nodes_controller_test.rb | 14 | ||||
| -rw-r--r-- | test/models/node_test.rb | 9 | ||||
| -rw-r--r-- | test/models/page_test.rb | 32 |
4 files changed, 54 insertions, 4 deletions
diff --git a/test/controllers/content_controller_test.rb b/test/controllers/content_controller_test.rb index e8bc55f..2449a0c 100644 --- a/test/controllers/content_controller_test.rb +++ b/test/controllers/content_controller_test.rb | |||
| @@ -73,8 +73,7 @@ class ContentControllerTest < ActionController::TestCase | |||
| 73 | def test_nonexistant_custom_template_defaults_to_standard_template | 73 | def test_nonexistant_custom_template_defaults_to_standard_template |
| 74 | new_node = create_node_under_root "fnord" | 74 | new_node = create_node_under_root "fnord" |
| 75 | draft = find_or_create_draft(new_node, @user1) | 75 | draft = find_or_create_draft(new_node, @user1) |
| 76 | draft.template_name = "huchibu" | 76 | draft.update_column(:template_name, "huchibu") |
| 77 | draft.save | ||
| 78 | new_node.publish_draft! | 77 | new_node.publish_draft! |
| 79 | 78 | ||
| 80 | get :render_page, params: { :locale => 'de', :page_path => ["fnord"] } | 79 | get :render_page, params: { :locale => 'de', :page_path => ["fnord"] } |
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb index 5b66bd3..b0d7416 100644 --- a/test/controllers/nodes_controller_test.rb +++ b/test/controllers/nodes_controller_test.rb | |||
| @@ -140,7 +140,7 @@ class NodesControllerTest < ActionController::TestCase | |||
| 140 | :page => { | 140 | :page => { |
| 141 | :title => "Hello", | 141 | :title => "Hello", |
| 142 | :body => "There", | 142 | :body => "There", |
| 143 | :template_name => "Foobar" | 143 | :template_name => "title_only" |
| 144 | } | 144 | } |
| 145 | } | 145 | } |
| 146 | 146 | ||
| @@ -148,7 +148,17 @@ class NodesControllerTest < ActionController::TestCase | |||
| 148 | test_node.reload | 148 | test_node.reload |
| 149 | assert_equal "Hello", test_node.head.title | 149 | assert_equal "Hello", test_node.head.title |
| 150 | assert_equal "There", test_node.head.body | 150 | assert_equal "There", test_node.head.body |
| 151 | assert_equal "Foobar", test_node.head.template_name | 151 | assert_equal "title_only", test_node.head.template_name |
| 152 | end | ||
| 153 | |||
| 154 | def test_update_rejects_a_template_name_not_on_disk | ||
| 155 | test_node = Node.root.children.create! :slug => "test_node" | ||
| 156 | login_as :quentin | ||
| 157 | put :update, params: { :id => test_node.id, | ||
| 158 | :page => { :title => "Hello", :template_name => "Foobar" } } | ||
| 159 | |||
| 160 | test_node.reload | ||
| 161 | assert_not_equal "Foobar", test_node.draft&.template_name | ||
| 152 | end | 162 | end |
| 153 | 163 | ||
| 154 | test "publish draft with staged_slug unqueal slug" do | 164 | test "publish draft with staged_slug unqueal slug" do |
diff --git a/test/models/node_test.rb b/test/models/node_test.rb index c8e49e5..4401651 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb | |||
| @@ -761,4 +761,13 @@ class NodeTest < ActiveSupport::TestCase | |||
| 761 | assert_equal "Second", action.metadata.dig("title", "from") | 761 | assert_equal "Second", action.metadata.dig("title", "from") |
| 762 | assert_equal "First", action.metadata.dig("title", "to") | 762 | assert_equal "First", action.metadata.dig("title", "to") |
| 763 | end | 763 | end |
| 764 | |||
| 765 | test "default_template_name rejects values not present in the template directory" do | ||
| 766 | node = Node.root.children.build(:slug => "template_guard_test", | ||
| 767 | :default_template_name => "../../layouts/admin") | ||
| 768 | assert_not node.valid? | ||
| 769 | |||
| 770 | node.default_template_name = "standard_template" | ||
| 771 | assert node.valid? | ||
| 772 | end | ||
| 764 | end | 773 | end |
diff --git a/test/models/page_test.rb b/test/models/page_test.rb index edb7c37..58794d5 100644 --- a/test/models/page_test.rb +++ b/test/models/page_test.rb | |||
| @@ -370,4 +370,36 @@ class PageTest < ActiveSupport::TestCase | |||
| 370 | assert en_entry[:changed] | 370 | assert en_entry[:changed] |
| 371 | refute en_entry[:exists_there] | 371 | refute en_entry[:exists_there] |
| 372 | end | 372 | end |
| 373 | |||
| 374 | test "aggregate ignores order_by values outside the allowlist" do | ||
| 375 | sql = Page.aggregate(:order_by => "pages.id; DROP TABLE pages--").to_sql | ||
| 376 | |||
| 377 | assert_not_includes sql, "DROP" | ||
| 378 | assert_includes sql, "pages.id ASC" | ||
| 379 | end | ||
| 380 | |||
| 381 | test "aggregate accepts allowlisted order columns, bare or prefixed" do | ||
| 382 | assert_includes Page.aggregate(:order_by => "published_at").to_sql, | ||
| 383 | "pages.published_at ASC" | ||
| 384 | assert_includes Page.aggregate(:order_by => "pages.published_at").to_sql, | ||
| 385 | "pages.published_at ASC" | ||
| 386 | end | ||
| 387 | |||
| 388 | test "template_name rejects values not present in the template directory" do | ||
| 389 | page = Page.create!(:title => "Template guard") | ||
| 390 | |||
| 391 | page.template_name = "../../partials/_article" | ||
| 392 | assert_not page.valid? | ||
| 393 | |||
| 394 | page.template_name = "standard_template" | ||
| 395 | assert page.valid? | ||
| 396 | end | ||
| 397 | |||
| 398 | test "a stale legacy template_name does not block unrelated saves" do | ||
| 399 | page = Page.create!(:title => "Stale template") | ||
| 400 | page.update_column(:template_name, "long_deleted_template") | ||
| 401 | |||
| 402 | page.reload | ||
| 403 | assert page.update(:abstract => "still saveable") | ||
| 404 | end | ||
| 373 | end | 405 | end |
