orb_template 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/lib/orb/version.rb +1 -1
- data/lib/orb.rb +6 -2
- metadata +3 -8
- data/CODE_OF_CONDUCT.md +0 -132
- data/Makefile +0 -57
- data/Rakefile +0 -21
- data/docs/2026-03-12-security-analysis.md +0 -715
- data/docs/2026-04-27-unwrap-poposal.md +0 -323
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9cef68c21c57f074ee08bb76aacb4d88df41ff5726ca65cf3ef1c579e5d56e8d
|
|
4
|
+
data.tar.gz: 3fb10347aada2d9fcd845e8590aaea771c2260b907cdc55f87927ff2afde44de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05debe94445dee051e126bf574ba1ed3d2ea93030fe86225261da50ae33af39c7d77b75d9b7f2607e98b655c8076c83a4dc5c111ef8810292353d1f2104fdb65
|
|
7
|
+
data.tar.gz: 9a2469f3a8a7379f84cbcf3bd78644b15f7bc89e5da2c350d73e9b82c60cbf8f97dff305bee834a470b4021c2a685aee672a1a6fb1097e0fdf9c7eef53eee0f2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-11-04
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **Breaking:** Require Ruby 3.4 or newer.
|
|
8
|
+
- `ORB.html_escape` now delegates to `Temple::Utils.escape_html`. (`lib/orb.rb`)
|
|
9
|
+
- `make test` now also runs the benchmark suite, and `make trivy` lists lower-severity findings without failing on them.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Stop loading `cgi/util`, which removes the load warning on Ruby 4.0. `ORB.html_escape` still handles non-ASCII-compatible strings such as UTF-16 and returns them in their original encoding. (`lib/orb.rb`)
|
|
14
|
+
- `ORB::VERSION` is now defined after `require "orb"`. Previously it raised `NameError` when the gem was loaded outside bundler. (`lib/orb.rb`)
|
|
15
|
+
|
|
3
16
|
## [0.2.5] - 2026-04-28
|
|
4
17
|
|
|
5
18
|
### Added
|
data/lib/orb/version.rb
CHANGED
data/lib/orb.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'temple'
|
|
4
|
-
require 'cgi/util'
|
|
5
4
|
|
|
5
|
+
require_relative "orb/version"
|
|
6
6
|
require_relative "orb/errors"
|
|
7
7
|
require_relative "orb/token"
|
|
8
8
|
require_relative "orb/tokenizer"
|
|
@@ -34,7 +34,11 @@ module ORB
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def self.html_escape(str)
|
|
37
|
-
|
|
37
|
+
str = str.to_s
|
|
38
|
+
return ::Temple::Utils.escape_html(str) if str.encoding.ascii_compatible?
|
|
39
|
+
|
|
40
|
+
# The native escaper can't handle encodings such as UTF-16 on Ruby 3.4.
|
|
41
|
+
::Temple::Utils.escape_html(str.encode(Encoding::UTF_8)).encode(str.encoding)
|
|
38
42
|
end
|
|
39
43
|
end
|
|
40
44
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: orb_template
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- KUY.io Inc.
|
|
@@ -34,13 +34,8 @@ extensions: []
|
|
|
34
34
|
extra_rdoc_files: []
|
|
35
35
|
files:
|
|
36
36
|
- CHANGELOG.md
|
|
37
|
-
- CODE_OF_CONDUCT.md
|
|
38
37
|
- LICENSE.txt
|
|
39
|
-
- Makefile
|
|
40
38
|
- README.md
|
|
41
|
-
- Rakefile
|
|
42
|
-
- docs/2026-03-12-security-analysis.md
|
|
43
|
-
- docs/2026-04-27-unwrap-poposal.md
|
|
44
39
|
- lib/orb.rb
|
|
45
40
|
- lib/orb/ast.rb
|
|
46
41
|
- lib/orb/ast/abstract_node.rb
|
|
@@ -93,14 +88,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
93
88
|
requirements:
|
|
94
89
|
- - ">="
|
|
95
90
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: 3.
|
|
91
|
+
version: '3.4'
|
|
97
92
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
93
|
requirements:
|
|
99
94
|
- - ">="
|
|
100
95
|
- !ruby/object:Gem::Version
|
|
101
96
|
version: '0'
|
|
102
97
|
requirements: []
|
|
103
|
-
rubygems_version: 4.0.
|
|
98
|
+
rubygems_version: 4.0.23
|
|
104
99
|
specification_version: 4
|
|
105
100
|
summary: The ORB template language for Ruby.
|
|
106
101
|
test_files: []
|
data/CODE_OF_CONDUCT.md
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
# Contributor Covenant Code of Conduct
|
|
2
|
-
|
|
3
|
-
## Our Pledge
|
|
4
|
-
|
|
5
|
-
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
-
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
-
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
-
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
-
nationality, personal appearance, race, caste, color, religion, or sexual
|
|
10
|
-
identity and orientation.
|
|
11
|
-
|
|
12
|
-
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
-
diverse, inclusive, and healthy community.
|
|
14
|
-
|
|
15
|
-
## Our Standards
|
|
16
|
-
|
|
17
|
-
Examples of behavior that contributes to a positive environment for our
|
|
18
|
-
community include:
|
|
19
|
-
|
|
20
|
-
* Demonstrating empathy and kindness toward other people
|
|
21
|
-
* Being respectful of differing opinions, viewpoints, and experiences
|
|
22
|
-
* Giving and gracefully accepting constructive feedback
|
|
23
|
-
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
24
|
-
and learning from the experience
|
|
25
|
-
* Focusing on what is best not just for us as individuals, but for the overall
|
|
26
|
-
community
|
|
27
|
-
|
|
28
|
-
Examples of unacceptable behavior include:
|
|
29
|
-
|
|
30
|
-
* The use of sexualized language or imagery, and sexual attention or advances of
|
|
31
|
-
any kind
|
|
32
|
-
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
33
|
-
* Public or private harassment
|
|
34
|
-
* Publishing others' private information, such as a physical or email address,
|
|
35
|
-
without their explicit permission
|
|
36
|
-
* Other conduct which could reasonably be considered inappropriate in a
|
|
37
|
-
professional setting
|
|
38
|
-
|
|
39
|
-
## Enforcement Responsibilities
|
|
40
|
-
|
|
41
|
-
Community leaders are responsible for clarifying and enforcing our standards of
|
|
42
|
-
acceptable behavior and will take appropriate and fair corrective action in
|
|
43
|
-
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
44
|
-
or harmful.
|
|
45
|
-
|
|
46
|
-
Community leaders have the right and responsibility to remove, edit, or reject
|
|
47
|
-
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
48
|
-
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
49
|
-
decisions when appropriate.
|
|
50
|
-
|
|
51
|
-
## Scope
|
|
52
|
-
|
|
53
|
-
This Code of Conduct applies within all community spaces, and also applies when
|
|
54
|
-
an individual is officially representing the community in public spaces.
|
|
55
|
-
Examples of representing our community include using an official email address,
|
|
56
|
-
posting via an official social media account, or acting as an appointed
|
|
57
|
-
representative at an online or offline event.
|
|
58
|
-
|
|
59
|
-
## Enforcement
|
|
60
|
-
|
|
61
|
-
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
62
|
-
reported to the community leaders responsible for enforcement at
|
|
63
|
-
[INSERT CONTACT METHOD].
|
|
64
|
-
All complaints will be reviewed and investigated promptly and fairly.
|
|
65
|
-
|
|
66
|
-
All community leaders are obligated to respect the privacy and security of the
|
|
67
|
-
reporter of any incident.
|
|
68
|
-
|
|
69
|
-
## Enforcement Guidelines
|
|
70
|
-
|
|
71
|
-
Community leaders will follow these Community Impact Guidelines in determining
|
|
72
|
-
the consequences for any action they deem in violation of this Code of Conduct:
|
|
73
|
-
|
|
74
|
-
### 1. Correction
|
|
75
|
-
|
|
76
|
-
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
77
|
-
unprofessional or unwelcome in the community.
|
|
78
|
-
|
|
79
|
-
**Consequence**: A private, written warning from community leaders, providing
|
|
80
|
-
clarity around the nature of the violation and an explanation of why the
|
|
81
|
-
behavior was inappropriate. A public apology may be requested.
|
|
82
|
-
|
|
83
|
-
### 2. Warning
|
|
84
|
-
|
|
85
|
-
**Community Impact**: A violation through a single incident or series of
|
|
86
|
-
actions.
|
|
87
|
-
|
|
88
|
-
**Consequence**: A warning with consequences for continued behavior. No
|
|
89
|
-
interaction with the people involved, including unsolicited interaction with
|
|
90
|
-
those enforcing the Code of Conduct, for a specified period of time. This
|
|
91
|
-
includes avoiding interactions in community spaces as well as external channels
|
|
92
|
-
like social media. Violating these terms may lead to a temporary or permanent
|
|
93
|
-
ban.
|
|
94
|
-
|
|
95
|
-
### 3. Temporary Ban
|
|
96
|
-
|
|
97
|
-
**Community Impact**: A serious violation of community standards, including
|
|
98
|
-
sustained inappropriate behavior.
|
|
99
|
-
|
|
100
|
-
**Consequence**: A temporary ban from any sort of interaction or public
|
|
101
|
-
communication with the community for a specified period of time. No public or
|
|
102
|
-
private interaction with the people involved, including unsolicited interaction
|
|
103
|
-
with those enforcing the Code of Conduct, is allowed during this period.
|
|
104
|
-
Violating these terms may lead to a permanent ban.
|
|
105
|
-
|
|
106
|
-
### 4. Permanent Ban
|
|
107
|
-
|
|
108
|
-
**Community Impact**: Demonstrating a pattern of violation of community
|
|
109
|
-
standards, including sustained inappropriate behavior, harassment of an
|
|
110
|
-
individual, or aggression toward or disparagement of classes of individuals.
|
|
111
|
-
|
|
112
|
-
**Consequence**: A permanent ban from any sort of public interaction within the
|
|
113
|
-
community.
|
|
114
|
-
|
|
115
|
-
## Attribution
|
|
116
|
-
|
|
117
|
-
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
118
|
-
version 2.1, available at
|
|
119
|
-
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
|
120
|
-
|
|
121
|
-
Community Impact Guidelines were inspired by
|
|
122
|
-
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
|
123
|
-
|
|
124
|
-
For answers to common questions about this code of conduct, see the FAQ at
|
|
125
|
-
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
|
126
|
-
[https://www.contributor-covenant.org/translations][translations].
|
|
127
|
-
|
|
128
|
-
[homepage]: https://www.contributor-covenant.org
|
|
129
|
-
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
|
130
|
-
[Mozilla CoC]: https://github.com/mozilla/diversity
|
|
131
|
-
[FAQ]: https://www.contributor-covenant.org/faq
|
|
132
|
-
[translations]: https://www.contributor-covenant.org/translations
|
data/Makefile
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# Makefile for ORB Ruby Gem
|
|
2
|
-
|
|
3
|
-
.PHONY: all test build clean install lint spec benchmark security help
|
|
4
|
-
|
|
5
|
-
# Default target
|
|
6
|
-
all: test build
|
|
7
|
-
|
|
8
|
-
# Run test suite (excludes benchmarks)
|
|
9
|
-
spec:
|
|
10
|
-
bundle exec rake test
|
|
11
|
-
|
|
12
|
-
# Run benchmark suite
|
|
13
|
-
benchmark:
|
|
14
|
-
bundle exec rake benchmark
|
|
15
|
-
|
|
16
|
-
# Build the gem
|
|
17
|
-
build:
|
|
18
|
-
gem build orb_template.gemspec
|
|
19
|
-
|
|
20
|
-
# Deploy to RubyGems (requires authentication)
|
|
21
|
-
deploy: build
|
|
22
|
-
gem push orb_template-*.gem
|
|
23
|
-
|
|
24
|
-
# Clean build artifacts
|
|
25
|
-
clean:
|
|
26
|
-
rm -f *.gem
|
|
27
|
-
|
|
28
|
-
# Install the gem locally
|
|
29
|
-
install: build
|
|
30
|
-
gem install orb_template-*.gem
|
|
31
|
-
|
|
32
|
-
# Run linting
|
|
33
|
-
lint:
|
|
34
|
-
bundle exec rubocop
|
|
35
|
-
|
|
36
|
-
# Security scanning (dependency audit, static analysis, filesystem scan)
|
|
37
|
-
security:
|
|
38
|
-
bundle exec bundle-audit check --update
|
|
39
|
-
bundle exec brakeman --no-pager -q -p test/dummy
|
|
40
|
-
trivy fs --scanners vuln,secret,misconfig .
|
|
41
|
-
|
|
42
|
-
# Run specs, linting, security, and benchmarks
|
|
43
|
-
test: spec lint security benchmark
|
|
44
|
-
|
|
45
|
-
# Show help
|
|
46
|
-
help:
|
|
47
|
-
@echo "Available targets:"
|
|
48
|
-
@echo " all - Run tests and build the gem (default)"
|
|
49
|
-
@echo " test - Run both specs and linting"
|
|
50
|
-
@echo " build - Build the gem package"
|
|
51
|
-
@echo " install - Build and install the gem locally"
|
|
52
|
-
@echo " clean - Remove built gem files"
|
|
53
|
-
@echo " spec - Run the test suite (excludes benchmarks)"
|
|
54
|
-
@echo " benchmark - Run the benchmark suite"
|
|
55
|
-
@echo " lint - Run RuboCop linting"
|
|
56
|
-
@echo " security - Run security scans (bundler-audit, brakeman, trivy)"
|
|
57
|
-
@echo " help - Show this help message"
|
data/Rakefile
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bundler/gem_tasks"
|
|
4
|
-
require "rubocop/rake_task"
|
|
5
|
-
require "rake/testtask"
|
|
6
|
-
|
|
7
|
-
RuboCop::RakeTask.new
|
|
8
|
-
|
|
9
|
-
Rake::TestTask.new(:test) do |t|
|
|
10
|
-
t.libs << "test"
|
|
11
|
-
t.libs << "lib"
|
|
12
|
-
t.test_files = FileList['test/**/*_test.rb'].exclude('test/benchmark_test.rb')
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
Rake::TestTask.new(:benchmark) do |t|
|
|
16
|
-
t.libs << "test"
|
|
17
|
-
t.libs << "lib"
|
|
18
|
-
t.test_files = FileList['test/benchmark_test.rb']
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
task default: %i[rubocop test]
|
|
@@ -1,715 +0,0 @@
|
|
|
1
|
-
# ORB Template Engine -- Security Analysis
|
|
2
|
-
|
|
3
|
-
**Date:** 2026-03-12
|
|
4
|
-
**Version Reviewed:** 0.1.3
|
|
5
|
-
**Scope:** Tokenizer2, Parser, AST Builder, Temple Compiler, Filters, Attributes Compiler, Rails Template Handler
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Executive Summary
|
|
10
|
-
|
|
11
|
-
ORB is a JSX-inspired template engine for Ruby/Rails that compiles templates to Ruby code via the Temple pipeline. The overall architecture is sound -- it uses Temple's `:escape` mechanism for auto-escaping `{{...}}` output and avoids direct `eval()` of raw user strings. However, this review identified several vulnerabilities ranging from **critical** to **low** severity, many of which mirror historical CVEs in ERB, HAML, and SLIM template engines.
|
|
12
|
-
|
|
13
|
-
The primary risk areas are:
|
|
14
|
-
|
|
15
|
-
- Code injection through directive values and expressions interpolated into generated Ruby code
|
|
16
|
-
- Missing HTML escaping on dynamic attribute values
|
|
17
|
-
- Insufficient validation of tag names, attribute names, and `:for` expressions
|
|
18
|
-
|
|
19
|
-
Note: Like ERB, HAML, and SLIM, ORB executes arbitrary Ruby in template expressions (`{{...}}` and `{%...%}`). This is by design and follows the same trust model -- templates are developer-authored and loaded from the filesystem. This is documented as informational, not as a vulnerability.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Data Flow Overview
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
ORB Template Source (text)
|
|
27
|
-
|
|
|
28
|
-
v
|
|
29
|
-
[Tokenizer2] lib/orb/tokenizer2.rb -- Lexical analysis via StringScanner
|
|
30
|
-
|
|
|
31
|
-
v
|
|
32
|
-
Token Stream
|
|
33
|
-
|
|
|
34
|
-
v
|
|
35
|
-
[Parser] lib/orb/parser.rb -- Token-to-AST conversion
|
|
36
|
-
|
|
|
37
|
-
v
|
|
38
|
-
AST (Abstract Syntax Tree)
|
|
39
|
-
|
|
|
40
|
-
v
|
|
41
|
-
[Temple::Compiler] lib/orb/temple/compiler.rb -- AST to Temple IR
|
|
42
|
-
|
|
|
43
|
-
v
|
|
44
|
-
[Temple::Filters] lib/orb/temple/filters.rb -- Component/block handling
|
|
45
|
-
|
|
|
46
|
-
v
|
|
47
|
-
[Temple Pipeline] lib/orb/temple/engine.rb -- Escaping, static analysis, optimization
|
|
48
|
-
|
|
|
49
|
-
v
|
|
50
|
-
Generated Ruby Code
|
|
51
|
-
|
|
|
52
|
-
v
|
|
53
|
-
[Rails ActionView] lib/orb/rails_template.rb -- Template handler execution
|
|
54
|
-
|
|
|
55
|
-
v
|
|
56
|
-
HTML Output
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Security-sensitive boundaries exist at every stage. The most critical are:
|
|
60
|
-
|
|
61
|
-
1. **Tokenizer -> Parser**: What syntax is accepted and how expressions are delimited
|
|
62
|
-
2. **Compiler -> Filters**: How expressions, directives, and attributes are interpolated into Ruby code
|
|
63
|
-
3. **Filters -> Temple Pipeline**: Whether dynamic values are wrapped in escape directives
|
|
64
|
-
4. **Generated Code -> Rails**: What code executes in the view binding
|
|
65
|
-
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
## INFORMATIONAL Findings
|
|
69
|
-
|
|
70
|
-
### INFO-1: Server-Side Template Injection via `{%...%}` Control Expressions
|
|
71
|
-
|
|
72
|
-
**Location:** `lib/orb/temple/compiler.rb:120-123`
|
|
73
|
-
|
|
74
|
-
Control expressions compile directly to `:code` Temple expressions, allowing arbitrary Ruby execution. This is equivalent to ERB's `<% %>`, HAML's `- code`, and SLIM's `- code` -- it is a fundamental feature of any code-executing template language, not a vulnerability in ORB specifically.
|
|
75
|
-
|
|
76
|
-
Templates are authored by developers and loaded from the filesystem via Rails' template resolver, which restricts to the application's view paths. The same trust model applies as with ERB.
|
|
77
|
-
|
|
78
|
-
**Mitigation:** Document that ORB templates must not be constructed from user input (same guidance as ERB). Long-term, consider Brakeman integration to flag unsafe patterns like `ORB::Template.parse(user_string)`.
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
|
|
82
|
-
### INFO-2: Server-Side Template Injection via `{{...}}` Printing Expressions
|
|
83
|
-
|
|
84
|
-
**Location:** `lib/orb/temple/compiler.rb:109`
|
|
85
|
-
|
|
86
|
-
Printing expressions execute arbitrary Ruby in the template binding (output is HTML-escaped). This is equivalent to ERB's `<%= %>`. Same trust model as INFO-1 applies.
|
|
87
|
-
|
|
88
|
-
**Mitigation:** Documentation only. Same as ERB.
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
### INFO-3: Error Messages Expose Internal Tokenizer State
|
|
93
|
-
|
|
94
|
-
**Location:** `lib/orb/tokenizer2.rb:617`
|
|
95
|
-
|
|
96
|
-
Error messages include internal tokenizer state names (`:printing_expression`, `:control_expression`, etc.). In development mode, this is intentional and helpful for debugging -- the same behavior as ERB, HAML, and SLIM. In production, Rails rescues exceptions and shows a generic error page, so these details are only visible in server logs (which are already privileged).
|
|
97
|
-
|
|
98
|
-
**Mitigation:** Standard Rails error handling. Only a concern if the application is misconfigured to expose exception details in production responses.
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## CRITICAL Findings
|
|
103
|
-
|
|
104
|
-
### CRITICAL-1: Code Injection via `:for` Directive Expression Splitting
|
|
105
|
-
|
|
106
|
-
**Location:** `lib/orb/temple/filters.rb:109-116`
|
|
107
|
-
|
|
108
|
-
The `:for` block handler uses a naive `split(' in ')` to separate the iterator variable from the collection:
|
|
109
|
-
|
|
110
|
-
```ruby
|
|
111
|
-
# lib/orb/temple/filters.rb:110-111
|
|
112
|
-
def on_orb_for(expression, content)
|
|
113
|
-
enumerator, collection = expression.split(' in ')
|
|
114
|
-
code = "#{collection}.each do |#{enumerator}|"
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Both `enumerator` and `collection` are interpolated into a Ruby code string without validation. A crafted `:for` expression can inject arbitrary code:
|
|
118
|
-
|
|
119
|
-
```orb
|
|
120
|
-
{#for x in [1]; system("pwned"); [2]}
|
|
121
|
-
...
|
|
122
|
-
{/for}
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
This generates: `[1]; system("pwned"); [2].each do |x|`
|
|
126
|
-
|
|
127
|
-
The `enumerator` side is also injectable:
|
|
128
|
-
|
|
129
|
-
```orb
|
|
130
|
-
{#for x| ; system("pwned") ; |y in items}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
**Impact:** Arbitrary code execution via crafted template source.
|
|
134
|
-
|
|
135
|
-
**Recommendation:** Parse and validate the `:for` expression with a strict regex that only allows `variable_name in expression` patterns, where `variable_name` must be a valid Ruby identifier. Additionally, reject semicolons in the collection expression to prevent statement injection.
|
|
136
|
-
|
|
137
|
-
**Mitigation (applied):** In `lib/orb/temple/filters.rb:109-121`, the `on_orb_for` method now uses a strict regex to parse the `:for` expression and rejects semicolons in the collection:
|
|
138
|
-
|
|
139
|
-
```ruby
|
|
140
|
-
# Before
|
|
141
|
-
def on_orb_for(expression, content)
|
|
142
|
-
enumerator, collection = expression.split(' in ')
|
|
143
|
-
code = "#{collection}.each do |#{enumerator}|"
|
|
144
|
-
|
|
145
|
-
# After
|
|
146
|
-
def on_orb_for(expression, content)
|
|
147
|
-
match = expression.match(/\A\s*([a-z_]\w*)\s+in\s+(.+)\z/m)
|
|
148
|
-
raise ORB::SyntaxError.new("Invalid :for expression: enumerator must be a valid Ruby identifier", 0) unless match
|
|
149
|
-
|
|
150
|
-
enumerator, collection = match[1], match[2]
|
|
151
|
-
|
|
152
|
-
if collection.include?(';')
|
|
153
|
-
raise ORB::SyntaxError.new("Invalid :for collection expression: semicolons are not allowed", 0)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
code = "#{collection}.each do |#{enumerator}|"
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
The enumerator is now validated as a Ruby identifier (`[a-z_]\w*`), preventing pipe-delimiter injection. The collection is checked for semicolons, preventing statement injection. Both attack vectors now raise `ORB::SyntaxError` at compile time. Normal `:for` usage (`{#for item in items}`) is unaffected.
|
|
160
|
-
|
|
161
|
-
**Evidence:** All 4 CRITICAL-1 tests now pass. Full test suite (123 runs) shows 14 expected failures for unmitigated findings, 0 errors, 0 regressions.
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
## HIGH Findings
|
|
166
|
-
|
|
167
|
-
### HIGH-1: XSS via Unescaped Dynamic Attribute Values
|
|
168
|
-
|
|
169
|
-
**Location:** `lib/orb/temple/attributes_compiler.rb:91-93`
|
|
170
|
-
**Analogous CVEs:** CVE-2017-1002201 (HAML attribute XSS), CVE-2016-6316 (Rails Action View attribute XSS)
|
|
171
|
-
|
|
172
|
-
Dynamic attribute expressions are emitted as `[:dynamic, ...]` without an explicit `[:escape, true, ...]` wrapper:
|
|
173
|
-
|
|
174
|
-
```ruby
|
|
175
|
-
# lib/orb/temple/attributes_compiler.rb:91-93
|
|
176
|
-
elsif attribute.expression?
|
|
177
|
-
[:html, :attr, attribute.name, [:dynamic, attribute.value]]
|
|
178
|
-
end
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Compare with printing expressions which always use `[:escape, true, [:dynamic, ...]]` (compiler.rb:109).
|
|
182
|
-
|
|
183
|
-
Whether this is safe depends on Temple's downstream `Escapable` filter configuration and how `[:html, :attr, ...]` is processed. If the value is not escaped by the pipeline, an attacker could inject through attribute context:
|
|
184
|
-
|
|
185
|
-
```orb
|
|
186
|
-
<div class={user_input}>
|
|
187
|
-
<!-- if user_input = '"><script>alert(1)</script><div class="' -->
|
|
188
|
-
<!-- renders: <div class=""><script>alert(1)</script><div class=""> -->
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
**Impact:** Cross-site scripting (XSS) if dynamic attribute values are not escaped by the Temple pipeline. Unlike the template-authoring-only findings, this is exploitable at **runtime** through malicious data in any variable used in a dynamic attribute -- no template source compromise required.
|
|
192
|
-
|
|
193
|
-
**Recommendation:** Explicitly wrap dynamic attribute values: `[:html, :attr, attribute.name, [:escape, true, [:dynamic, attribute.value]]]`
|
|
194
|
-
|
|
195
|
-
**Mitigation (applied):** In `lib/orb/temple/attributes_compiler.rb:92`, the `compile_attribute` method now wraps expression attributes with `[:escape, true, ...]`:
|
|
196
|
-
|
|
197
|
-
```ruby
|
|
198
|
-
# Before
|
|
199
|
-
[:html, :attr, attribute.name, [:dynamic, attribute.value]]
|
|
200
|
-
|
|
201
|
-
# After
|
|
202
|
-
[:html, :attr, attribute.name, [:escape, true, [:dynamic, attribute.value]]]
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
This causes Temple's `Escapable` filter to emit `::Temple::Utils.escape_html(...)` around dynamic attribute values, matching the escaping behavior already applied to printing expressions (`{{...}}`). Static and boolean attributes are unaffected. Existing assertions in `test/temple_compiler_test.rb` were updated to expect the new escaped IR.
|
|
206
|
-
|
|
207
|
-
**Evidence:** `test_dynamic_attribute_value_is_escaped` now passes. Full test suite (123 runs) shows no regressions beyond the 18 expected security test failures for unmitigated findings.
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
### HIGH-2: Code Injection via `:with` Directive in Block Parameters
|
|
212
|
-
|
|
213
|
-
**Location:** `lib/orb/temple/filters.rb:37, 46, 80, 83`
|
|
214
|
-
|
|
215
|
-
The `:with` directive value is used directly as a Ruby block parameter name without validation:
|
|
216
|
-
|
|
217
|
-
```ruby
|
|
218
|
-
# lib/orb/temple/filters.rb:37
|
|
219
|
-
block_name = node.directives.fetch(:with, block_name)
|
|
220
|
-
# ...
|
|
221
|
-
# lib/orb/temple/filters.rb:46
|
|
222
|
-
code = "render #{komponent_name}.new(#{args}) do |#{block_name}|"
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
A crafted `:with` directive can inject code into the block parameter position:
|
|
226
|
-
|
|
227
|
-
```orb
|
|
228
|
-
<MyComponent :with="x| ; system('pwned') ; |y">
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
Generates: `render MyComponent.new() do |x| ; system('pwned') ; |y|`
|
|
232
|
-
|
|
233
|
-
The same vulnerability exists in slot rendering (filters.rb:80-83).
|
|
234
|
-
|
|
235
|
-
**Impact:** Arbitrary code execution via crafted template source.
|
|
236
|
-
|
|
237
|
-
**Recommendation:** Validate that `:with` values match `/\A[a-z_][a-zA-Z0-9_]*\z/` (valid Ruby identifier).
|
|
238
|
-
|
|
239
|
-
**Mitigation (applied):** In `lib/orb/temple/filters.rb`, both `on_orb_component` and `on_orb_slot` now validate the block name after resolving the `:with` directive:
|
|
240
|
-
|
|
241
|
-
```ruby
|
|
242
|
-
block_name = node.directives.fetch(:with, block_name)
|
|
243
|
-
unless block_name.match?(/\A[a-z_]\w*\z/)
|
|
244
|
-
raise ORB::SyntaxError.new("Invalid :with directive value: must be a valid Ruby identifier", 0)
|
|
245
|
-
end
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
This validation applies to both explicit `:with` values and the auto-generated default block names, providing defense-in-depth against malicious tag/slot names that produce invalid default identifiers (also catches HIGH-4 and HIGH-5 attack vectors as a side effect).
|
|
249
|
-
|
|
250
|
-
**Evidence:** `test_with_directive_component_injection_is_rejected` and `test_with_directive_slot_injection_is_rejected` now pass. Full test suite (86 non-security runs) shows no regressions.
|
|
251
|
-
|
|
252
|
-
---
|
|
253
|
-
|
|
254
|
-
### HIGH-3: Unsafe Tag Name Interpolation in Dynamic Tags
|
|
255
|
-
|
|
256
|
-
**Location:** `lib/orb/temple/filters.rb:130`
|
|
257
|
-
|
|
258
|
-
```ruby
|
|
259
|
-
code = "content_tag('#{node.tag}', #{splats}) do"
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
The tag name is interpolated into a single-quoted Ruby string. The `TAG_NAME` pattern (`[^\s>/=$]+` in patterns.rb:6) allows single quotes, semicolons, and parentheses. Since spaces are not allowed in tag names, the injection must be crafted without spaces:
|
|
263
|
-
|
|
264
|
-
```orb
|
|
265
|
-
<div');system(:pwned);content_tag('x **{attrs}>content</div');system(:pwned);content_tag('x>
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
This generates:
|
|
269
|
-
|
|
270
|
-
```ruby
|
|
271
|
-
content_tag('div');system(:pwned);content_tag('x', **attrs) do
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
**Impact:** Arbitrary code execution via crafted tag name in templates using splat attributes.
|
|
275
|
-
|
|
276
|
-
**Recommendation:** Validate tag names against a strict pattern (alphanumeric, hyphens, dots, colons only) before interpolating into generated code.
|
|
277
|
-
|
|
278
|
-
**Mitigation (applied):** In `lib/orb/temple/filters.rb`, a `VALID_HTML_TAG_NAME` constant (`/\A[a-zA-Z][a-zA-Z0-9-]*\z/`) is declared and checked in `on_orb_dynamic` before the tag name is interpolated:
|
|
279
|
-
|
|
280
|
-
```ruby
|
|
281
|
-
VALID_HTML_TAG_NAME = /\A[a-zA-Z][a-zA-Z0-9-]*\z/
|
|
282
|
-
|
|
283
|
-
# In on_orb_dynamic, HTML tag branch:
|
|
284
|
-
unless node.tag.match?(VALID_HTML_TAG_NAME)
|
|
285
|
-
raise ORB::SyntaxError.new("Invalid tag name: #{node.tag.inspect}", 0)
|
|
286
|
-
end
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
This allows standard HTML tags (`div`, `h1`, `my-element`) but rejects quotes, semicolons, parentheses, and other characters that could break out of the generated string literal. Components and slots are dispatched before this branch and are not affected.
|
|
290
|
-
|
|
291
|
-
**Evidence:** `test_dynamic_tag_name_injection_is_rejected` and `test_dynamic_tag_name_with_quote_is_rejected` now pass. Full test suite (86 non-security runs) shows no regressions.
|
|
292
|
-
|
|
293
|
-
---
|
|
294
|
-
|
|
295
|
-
### HIGH-4: Unvalidated Component Name Used as Ruby Constant
|
|
296
|
-
|
|
297
|
-
**Location:** `lib/orb/temple/filters.rb:32-34`
|
|
298
|
-
|
|
299
|
-
```ruby
|
|
300
|
-
name = node.tag.gsub('.', '::')
|
|
301
|
-
komponent = ORB.lookup_component(name)
|
|
302
|
-
komponent_name = komponent || name # Falls back to raw tag name
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
If a component is not found via `lookup_component`, the raw tag name (with `.` replaced by `::`) is used directly in a `render` call:
|
|
306
|
-
|
|
307
|
-
```ruby
|
|
308
|
-
code = "render #{komponent_name}.new(#{args}) do |#{block_name}|"
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
This can be exploited to call arbitrary methods by leveraging the `.new()` chain. The `TAG_NAME` pattern allows parentheses, so a component name like `Kernel.exit(1)` generates:
|
|
312
|
-
|
|
313
|
-
```ruby
|
|
314
|
-
render Kernel::exit(1).new() do |...|
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
`Kernel::exit(1)` executes immediately (terminating the process) before `.new()` is ever reached.
|
|
318
|
-
|
|
319
|
-
**Impact:** Arbitrary method calls on any Ruby constant reachable in the namespace. Process termination, code execution, or other side effects depending on the target class/method.
|
|
320
|
-
|
|
321
|
-
**Recommendation:** Validate that resolved component names only contain expected characters (`A-Z`, `a-z`, `0-9`, `::`) and optionally maintain an allowlist of component namespaces.
|
|
322
|
-
|
|
323
|
-
**Mitigation (applied):** In `lib/orb/temple/filters.rb`, a `VALID_COMPONENT_NAME` constant (`/\A[A-Z]\w*(::[A-Z]\w*)*\z/`) is declared and checked in `on_orb_component` after name resolution:
|
|
324
|
-
|
|
325
|
-
```ruby
|
|
326
|
-
VALID_COMPONENT_NAME = /\A[A-Z]\w*(::[A-Z]\w*)*\z/
|
|
327
|
-
|
|
328
|
-
# In on_orb_component:
|
|
329
|
-
komponent_name = komponent || name
|
|
330
|
-
unless komponent_name.match?(VALID_COMPONENT_NAME)
|
|
331
|
-
raise ORB::SyntaxError.new("Invalid component name: #{komponent_name.inspect}", 0)
|
|
332
|
-
end
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
This allows `Card`, `Demo::Card`, `UI::Forms::Input` but rejects names containing parentheses, semicolons, or lowercase-starting segments. The validation runs on the *resolved* name (after `lookup_component`), covering both the lookup result and the raw fallback.
|
|
336
|
-
|
|
337
|
-
**Breaking change:** Templates using `<Foo::bar>` style slot syntax will now raise a `SyntaxError`. The canonical slot syntax `<Foo:Bar>` should be used instead.
|
|
338
|
-
|
|
339
|
-
**Evidence:** `test_component_name_method_call_injection_is_rejected` and `test_component_name_semicolon_injection_is_rejected` pass (now with proper validation, not just the HIGH-2 side effect). The `:with` bypass is also blocked. Full test suite (86 non-security runs) shows no regressions.
|
|
340
|
-
|
|
341
|
-
---
|
|
342
|
-
|
|
343
|
-
### HIGH-5: Code Injection via Unvalidated Slot Names
|
|
344
|
-
|
|
345
|
-
**Location:** `lib/orb/temple/filters.rb:78-83`
|
|
346
|
-
|
|
347
|
-
Slot names are derived from the tag name portion after `:` (e.g. `Card:Header` -> slot `header`) and interpolated into a method call without validation:
|
|
348
|
-
|
|
349
|
-
```ruby
|
|
350
|
-
# lib/orb/temple/filters.rb:78-83
|
|
351
|
-
slot_name = node.slot # tag.split(':').last.underscore
|
|
352
|
-
code = "#{parent_name}.with_#{slot_name}(#{args}) do |#{block_name}|"
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
The `TAG_NAME` pattern (`[^\s>/=$]+`) allows semicolons, parentheses, and other characters that are not valid in Ruby method names. A crafted slot name can inject arbitrary code:
|
|
356
|
-
|
|
357
|
-
```orb
|
|
358
|
-
<Card><Card:Foo();system(1);x>content</Card:Foo();system(1);x></Card>
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
This generates:
|
|
362
|
-
|
|
363
|
-
```ruby
|
|
364
|
-
__orb__card.with_foo();system(1);x() do |__orb__foo();system(1);x|
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
The `with_foo()` call closes normally, then `system(1)` executes, then `x()` begins a new expression that absorbs the rest of the generated code.
|
|
368
|
-
|
|
369
|
-
**Impact:** Arbitrary code execution via crafted template source.
|
|
370
|
-
|
|
371
|
-
**Recommendation:** Validate slot names against a strict pattern (valid Ruby identifier: `/\A[a-z_][a-zA-Z0-9_]*\z/`) after extraction from the tag name.
|
|
372
|
-
|
|
373
|
-
**Mitigation (applied):** In `lib/orb/temple/filters.rb`, a `VALID_SLOT_NAME` constant (`/\A[a-z_]\w*\z/`) is declared and checked in `on_orb_slot` after extracting the slot name:
|
|
374
|
-
|
|
375
|
-
```ruby
|
|
376
|
-
VALID_SLOT_NAME = /\A[a-z_]\w*\z/
|
|
377
|
-
|
|
378
|
-
# In on_orb_slot:
|
|
379
|
-
slot_name = node.slot
|
|
380
|
-
unless slot_name.match?(VALID_SLOT_NAME)
|
|
381
|
-
raise ORB::SyntaxError.new("Invalid slot name: #{slot_name.inspect}", 0)
|
|
382
|
-
end
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
This allows `header`, `side_bar`, `footer_content` but rejects parentheses, semicolons, and anything that isn't a plain Ruby identifier. The validation is independent of the HIGH-2 `:with` check, so it cannot be bypassed by supplying a valid `:with` directive.
|
|
386
|
-
|
|
387
|
-
**Evidence:** `test_slot_name_semicolon_injection_is_rejected` and `test_slot_name_with_parens_is_rejected` pass. The `:with` bypass is also blocked. Full test suite (86 non-security runs) shows no regressions.
|
|
388
|
-
|
|
389
|
-
---
|
|
390
|
-
|
|
391
|
-
## MEDIUM Findings
|
|
392
|
-
|
|
393
|
-
### MEDIUM-1: Denial of Service via Unbounded Brace Nesting
|
|
394
|
-
|
|
395
|
-
**Location:** `lib/orb/tokenizer2.rb:260-284`
|
|
396
|
-
|
|
397
|
-
The brace-tracking mechanism (`@braces` array) has no depth limit. A malicious template with deeply nested braces consumes unbounded memory:
|
|
398
|
-
|
|
399
|
-
```
|
|
400
|
-
{{ {{{{{{{{{{{...millions of opening braces...}}}}}}}}}}} }}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
**Impact:** Memory exhaustion, denial of service.
|
|
404
|
-
|
|
405
|
-
**Recommendation:** Enforce a maximum nesting depth (e.g., 100 levels) and raise a `SyntaxError` when exceeded.
|
|
406
|
-
|
|
407
|
-
**Mitigation (applied):** In `lib/orb/tokenizer2.rb`, a `MAX_BRACE_DEPTH` constant (100) is declared and enforced via a `push_brace` helper that replaces all direct `@braces << "{"` calls:
|
|
408
|
-
|
|
409
|
-
```ruby
|
|
410
|
-
MAX_BRACE_DEPTH = 100
|
|
411
|
-
|
|
412
|
-
def push_brace
|
|
413
|
-
if @braces.length >= MAX_BRACE_DEPTH
|
|
414
|
-
raise ORB::SyntaxError.new("Maximum brace nesting depth (#{MAX_BRACE_DEPTH}) exceeded", @line)
|
|
415
|
-
end
|
|
416
|
-
@braces << "{"
|
|
417
|
-
end
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
All 5 brace-push sites in the tokenizer (`next_in_attribute_value_expression`, `next_in_splat_attribute_expression`, `next_in_block_open_content`, `next_in_printing_expression`, `next_in_control_expression`) now call `push_brace` instead of pushing directly. Moderate nesting (e.g., nested hashes) works fine; only pathological depths are rejected.
|
|
421
|
-
|
|
422
|
-
**Evidence:** `test_deeply_nested_braces_in_expression_raises_error` and `test_deeply_nested_braces_in_attribute_raises_error` now pass. `test_moderate_brace_nesting_works` remains green. Full test suite (86 non-security runs) shows no regressions.
|
|
423
|
-
|
|
424
|
-
---
|
|
425
|
-
|
|
426
|
-
### MEDIUM-2: OpenStruct-based RenderContext Exposes Introspection
|
|
427
|
-
|
|
428
|
-
**Location:** `lib/orb/render_context.rb:26`
|
|
429
|
-
|
|
430
|
-
```ruby
|
|
431
|
-
OpenStruct.new(@assigns).instance_eval { binding }
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
`OpenStruct` inherits from `Object`, exposing all `Object` methods to template expressions:
|
|
435
|
-
|
|
436
|
-
```
|
|
437
|
-
{{ self.class }} # => OpenStruct
|
|
438
|
-
{{ self.class.ancestors }} # => full class hierarchy
|
|
439
|
-
{{ instance_variable_get(:@table) }} # => all assigns as hash
|
|
440
|
-
{{ self.methods.sort }} # => available methods
|
|
441
|
-
{{ self.send(:system, "id") }} # => command execution via send
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
**Impact:** Template expressions (in the standalone `Template` class path) can introspect and exploit the full Ruby object model.
|
|
445
|
-
|
|
446
|
-
**Recommendation:** Use a `BasicObject` subclass instead of `OpenStruct` to minimize the available attack surface. Only expose explicitly allowed methods.
|
|
447
|
-
|
|
448
|
-
---
|
|
449
|
-
|
|
450
|
-
### MEDIUM-3: `runtime_error` String Delimiter Breakout
|
|
451
|
-
|
|
452
|
-
**Location:** `lib/orb/temple/compiler.rb:199`
|
|
453
|
-
|
|
454
|
-
```ruby
|
|
455
|
-
temple << [:code, %[raise ORB::Error.new(%q[#{error.message}], #{error.line.inspect})]]
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
Error messages are interpolated into a `%q[...]` string literal. The `%q[]` delimiter uses square brackets, meaning an error message containing `]` would prematurely close the string, potentially allowing code injection via crafted error messages.
|
|
459
|
-
|
|
460
|
-
**Impact:** If an attacker can trigger a specific error message containing `]`, the generated code could be malformed or injectable.
|
|
461
|
-
|
|
462
|
-
**Recommendation:** Use a delimiter that cannot appear in error messages (e.g., `%q{...}` with brace counting, or properly escape the content), or use `String#inspect` to safely serialize the message.
|
|
463
|
-
|
|
464
|
-
**Mitigation (applied):** In `lib/orb/temple/compiler.rb:199`, replaced `%q[#{error.message}]` with `#{error.message.inspect}`:
|
|
465
|
-
|
|
466
|
-
```ruby
|
|
467
|
-
# Before
|
|
468
|
-
temple << [:code, %[raise ORB::Error.new(%q[#{error.message}], #{error.line.inspect})]]
|
|
469
|
-
|
|
470
|
-
# After
|
|
471
|
-
temple << [:code, "raise ORB::Error.new(#{error.message.inspect}, #{error.line.inspect})"]
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
`String#inspect` produces a properly escaped double-quoted Ruby string literal. Any `]`, `"`, `\`, or other special characters are escaped, making delimiter breakout impossible. The generated code is always a single valid `raise` statement.
|
|
475
|
-
|
|
476
|
-
**Evidence:** `test_runtime_error_with_bracket_produces_valid_ruby` and `test_runtime_error_code_injection_is_rejected` now pass. Full test suite (86 non-security runs) shows no regressions.
|
|
477
|
-
|
|
478
|
-
---
|
|
479
|
-
|
|
480
|
-
### MEDIUM-4: Attribute Name Injection
|
|
481
|
-
|
|
482
|
-
**Location:** `lib/orb/patterns.rb:7`
|
|
483
|
-
|
|
484
|
-
```ruby
|
|
485
|
-
ATTRIBUTE_NAME = %r{[^\s>/=]+}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
This pattern allows nearly any character in attribute names, including:
|
|
489
|
-
|
|
490
|
-
- Event handlers: `onclick`, `onmouseover`, `onfocus`
|
|
491
|
-
- Quote characters: `"`, `'` (could break attribute context)
|
|
492
|
-
- Backticks, semicolons, and other special characters
|
|
493
|
-
|
|
494
|
-
**Analogous CVE:** CVE-2017-1002201 (HAML attribute injection via unescaped characters)
|
|
495
|
-
|
|
496
|
-
While ORB templates are typically authored by developers (not end users), the permissive pattern means:
|
|
497
|
-
|
|
498
|
-
1. No compile-time validation catches typos that could be security-relevant
|
|
499
|
-
2. If attribute names ever come from dynamic sources (e.g., splat attributes), injection is possible
|
|
500
|
-
|
|
501
|
-
**Recommendation:** Restrict `ATTRIBUTE_NAME` to valid HTML attribute characters: `/[a-zA-Z_:][-a-zA-Z0-9_:.]*` per the HTML spec, or at minimum disallow quotes and backticks.
|
|
502
|
-
|
|
503
|
-
**Mitigation (applied):** In `lib/orb/patterns.rb:7`, replaced the permissive pattern with a strict HTML-spec-compliant one:
|
|
504
|
-
|
|
505
|
-
```ruby
|
|
506
|
-
# Before
|
|
507
|
-
ATTRIBUTE_NAME = %r{[^\s>/=]+}
|
|
508
|
-
|
|
509
|
-
# After
|
|
510
|
-
ATTRIBUTE_NAME = %r{[a-zA-Z_:][-a-zA-Z0-9_:.]*}
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
This allows standard attribute names (`class`, `data-value`, `aria-label`, `xml:lang`) and ORB directives (`:for`, `:with`) while rejecting quotes, backticks, semicolons, and other characters that could break HTML attribute context. Invalid characters in attribute position are now silently ignored by the tokenizer (the scanner won't match them as attribute names).
|
|
514
|
-
|
|
515
|
-
**Evidence:** `test_attribute_name_with_single_quote_is_rejected`, `test_attribute_name_with_backtick_is_rejected`, and `test_attribute_name_with_double_quote_is_rejected` all pass. `test_valid_attribute_names_compile_correctly` remains green. Full test suite (86 non-security runs) shows no regressions.
|
|
516
|
-
|
|
517
|
-
---
|
|
518
|
-
|
|
519
|
-
## LOW Findings
|
|
520
|
-
|
|
521
|
-
### LOW-1: Verbatim Mode Bypasses All Processing
|
|
522
|
-
|
|
523
|
-
**Location:** `lib/orb/tokenizer2.rb:146-152`
|
|
524
|
-
|
|
525
|
-
The `$>` closing syntax enables verbatim mode, where content passes through without any processing or escaping:
|
|
526
|
-
|
|
527
|
-
```orb
|
|
528
|
-
<script$>
|
|
529
|
-
var x = "user controlled data here passes through raw";
|
|
530
|
-
</script>
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
This is intentional behavior for `<script>` and `<style>` tags, but developers unfamiliar with ORB may not realize that verbatim content is never escaped.
|
|
534
|
-
|
|
535
|
-
**Recommendation:** Document this behavior clearly. Consider requiring an explicit opt-in rather than a syntactic marker.
|
|
536
|
-
|
|
537
|
-
---
|
|
538
|
-
|
|
539
|
-
### LOW-2: Public Comments Pass Content as Static (Unescaped)
|
|
540
|
-
|
|
541
|
-
**Location:** `lib/orb/temple/compiler.rb:155`
|
|
542
|
-
|
|
543
|
-
```ruby
|
|
544
|
-
def transform_public_comment_node(node, _context)
|
|
545
|
-
[:html, :comment, [:static, node.text]]
|
|
546
|
-
end
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
Comment text is emitted as `[:static, ...]`. If comment content somehow contains `-->`, it could break out of the HTML comment context. However, since the tokenizer terminates comment parsing at `-->`, the text content would not contain this sequence under normal operation.
|
|
550
|
-
|
|
551
|
-
**Impact:** Minimal under current tokenizer behavior. Only relevant if the tokenizer is bypassed or modified.
|
|
552
|
-
|
|
553
|
-
---
|
|
554
|
-
|
|
555
|
-
### LOW-3: No Template Size Limit
|
|
556
|
-
|
|
557
|
-
**Location:** `lib/orb/tokenizer2.rb:26-27`
|
|
558
|
-
|
|
559
|
-
```ruby
|
|
560
|
-
def initialize(source, options = {})
|
|
561
|
-
@source = StringScanner.new(source)
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
There is no limit on template source size. An extremely large template could cause excessive memory usage and CPU consumption during tokenization.
|
|
565
|
-
|
|
566
|
-
**Recommendation:** Consider enforcing a configurable maximum template size.
|
|
567
|
-
|
|
568
|
-
**Mitigation (applied):** In `lib/orb/tokenizer2.rb`, a `MAX_TEMPLATE_SIZE` constant (2MB) is declared and checked at the start of `tokenize`:
|
|
569
|
-
|
|
570
|
-
```ruby
|
|
571
|
-
MAX_TEMPLATE_SIZE = 2 * 1024 * 1024 # 2MB
|
|
572
|
-
|
|
573
|
-
def tokenize
|
|
574
|
-
if @source.string.bytesize > MAX_TEMPLATE_SIZE
|
|
575
|
-
raise ORB::SyntaxError.new("Template exceeds maximum size (#{MAX_TEMPLATE_SIZE} bytes)", 0)
|
|
576
|
-
end
|
|
577
|
-
# ...
|
|
578
|
-
```
|
|
579
|
-
|
|
580
|
-
2MB is generous for any real template while preventing abuse. Templates exceeding this limit are rejected before any scanning begins.
|
|
581
|
-
|
|
582
|
-
**Evidence:** `test_large_template_has_size_limit` now passes. `test_normal_sized_template_works` remains green. Full test suite (86 non-security runs) shows no regressions.
|
|
583
|
-
|
|
584
|
-
---
|
|
585
|
-
|
|
586
|
-
### LOW-4: Potential ReDoS in Block Detection Regex
|
|
587
|
-
|
|
588
|
-
**Location:** `lib/orb/ast/printing_expression_node.rb:7`
|
|
589
|
-
|
|
590
|
-
```ruby
|
|
591
|
-
BLOCK_RE = /\A(if|unless)\b|\bdo\s*(\|[^|]*\|)?\s*$/
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
The `\s*` quantifiers combined with `$` could cause quadratic backtracking on inputs with many trailing whitespace characters followed by a non-matching character. The practical impact is limited because expression values are typically short.
|
|
595
|
-
|
|
596
|
-
**Recommendation:** Use possessive quantifiers or atomic groups if available: `\s*+` or `(?>\\s*)`.
|
|
597
|
-
|
|
598
|
-
---
|
|
599
|
-
|
|
600
|
-
## Comparison with Historical CVEs
|
|
601
|
-
|
|
602
|
-
| CVE | Engine | Vulnerability | ORB Applicability |
|
|
603
|
-
|-----|--------|--------------|-------------------|
|
|
604
|
-
| **CVE-2016-0752** | ERB/Rails | SSTI via dynamic `render` with user input | **Informational** -- `{%...%}` and `{{...}}` allow arbitrary Ruby execution, same as ERB (INFO-1, INFO-2) |
|
|
605
|
-
| **CVE-2017-1002201** | HAML | XSS via unescaped apostrophes in attributes | **Applicable** -- dynamic attribute values may lack escaping (HIGH-1); attribute names allow quotes (MEDIUM-4) |
|
|
606
|
-
| **CVE-2016-6316** | Rails/HAML | XSS in `html_safe` attribute values in tag helpers | **Partially applicable** -- interaction with `use_html_safe: true` option and Temple escaping pipeline needs verification (HIGH-1) |
|
|
607
|
-
| **CVE-2019-5418** | Rails | Path traversal via `render file:` with user input | **Not applicable** -- ORB does not support file includes or partial rendering by path |
|
|
608
|
-
| **CVE-2021-32818** | haml-coffee | RCE via configuration parameter pollution | **Not applicable** -- different architecture, no user-accessible configuration |
|
|
609
|
-
| SLIM XSS (Sqreen) | SLIM | XSS through `==` unescaped output and attribute injection | **Partially applicable** -- ORB's `{%...%}` is analogous to SLIM's `==` (no output escaping for control expressions) |
|
|
610
|
-
|
|
611
|
-
---
|
|
612
|
-
|
|
613
|
-
## Recommendations (Priority Order)
|
|
614
|
-
|
|
615
|
-
### Immediate (Before Public Release)
|
|
616
|
-
|
|
617
|
-
1. **Document SSTI risk** -- Add a Security section to README warning that ORB templates must never be constructed from user input. This is the single most important action.
|
|
618
|
-
|
|
619
|
-
2. **Escape dynamic attribute values** -- In `attributes_compiler.rb:91-93`, wrap dynamic attribute values:
|
|
620
|
-
```ruby
|
|
621
|
-
[:html, :attr, attribute.name, [:escape, true, [:dynamic, attribute.value]]]
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
3. **Validate `:for` expression syntax** -- Replace `split(' in ')` with a strict parser:
|
|
625
|
-
```ruby
|
|
626
|
-
match = expression.match(/\A\s*([a-z_]\w*)\s+in\s+(.+)\z/m)
|
|
627
|
-
raise CompilerError, "Invalid :for expression" unless match
|
|
628
|
-
enumerator, collection = match[1], match[2]
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
4. **Validate `:with` directive values** -- Ensure block parameter names are valid Ruby identifiers:
|
|
632
|
-
```ruby
|
|
633
|
-
unless block_name.match?(/\A[a-z_]\w*\z/)
|
|
634
|
-
raise CompilerError, "Invalid :with value: must be a valid identifier"
|
|
635
|
-
end
|
|
636
|
-
```
|
|
637
|
-
|
|
638
|
-
5. **Validate tag names before code interpolation** -- In `filters.rb`, validate before string interpolation:
|
|
639
|
-
```ruby
|
|
640
|
-
unless node.tag.match?(/\A[a-zA-Z][a-zA-Z0-9._:-]*\z/)
|
|
641
|
-
raise CompilerError, "Invalid tag name: #{node.tag}"
|
|
642
|
-
end
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
6. **Validate slot names** -- After extracting the slot name from the tag, validate it is a valid Ruby identifier:
|
|
646
|
-
```ruby
|
|
647
|
-
unless slot_name.match?(/\A[a-z_][a-zA-Z0-9_]*\z/)
|
|
648
|
-
raise CompilerError, "Invalid slot name: #{slot_name}"
|
|
649
|
-
end
|
|
650
|
-
```
|
|
651
|
-
|
|
652
|
-
### Short Term
|
|
653
|
-
|
|
654
|
-
7. **Fix `runtime_error` string delimiter** -- Use `String#inspect` for safe serialization of error messages.
|
|
655
|
-
|
|
656
|
-
8. **Add brace nesting depth limit** -- Cap at a reasonable depth (e.g., 100) in the tokenizer.
|
|
657
|
-
|
|
658
|
-
9. **Restrict `ATTRIBUTE_NAME` pattern** -- Use `/[a-zA-Z_:][-a-zA-Z0-9_:.]*` or similar.
|
|
659
|
-
|
|
660
|
-
### Long Term
|
|
661
|
-
|
|
662
|
-
10. **Consider a restricted execution mode** -- A "safe mode" that limits available methods in template expressions, similar to Liquid's approach.
|
|
663
|
-
|
|
664
|
-
11. **Integrate with Brakeman** -- Add ORB-specific checks for common vulnerability patterns.
|
|
665
|
-
|
|
666
|
-
12. **Replace `OpenStruct` in `RenderContext`** -- Use a `BasicObject` subclass to minimize attack surface for standalone template rendering.
|
|
667
|
-
|
|
668
|
-
13. **Add template size and complexity limits** -- Configurable maximums for source size, nesting depth, and expression count.
|
|
669
|
-
|
|
670
|
-
---
|
|
671
|
-
|
|
672
|
-
## Test Coverage
|
|
673
|
-
|
|
674
|
-
All findings are covered by regression tests in `test/security_test.rb`. Tests are written to **fail** until the corresponding fix is applied, then pass once mitigated.
|
|
675
|
-
|
|
676
|
-
| Finding | Test(s) | Status |
|
|
677
|
-
|---------|---------|--------|
|
|
678
|
-
| **CRITICAL-1** (:for injection) | `test_for_collection_injection_is_rejected`, `test_for_collection_injection_absent_from_temple_ir`, `test_for_enumerator_injection_is_rejected`, `test_for_directive_attribute_injection_is_rejected` | PASSING (4) -- mitigated |
|
|
679
|
-
| **HIGH-1** (attribute XSS) | `test_dynamic_attribute_value_is_escaped` | PASSING (1) -- mitigated |
|
|
680
|
-
| **HIGH-2** (:with injection) | `test_with_directive_component_injection_is_rejected`, `test_with_directive_slot_injection_is_rejected` | PASSING (2) -- mitigated |
|
|
681
|
-
| **HIGH-3** (tag name injection) | `test_dynamic_tag_name_injection_is_rejected`, `test_dynamic_tag_name_with_quote_is_rejected` | PASSING (2) -- mitigated |
|
|
682
|
-
| **HIGH-4** (component name injection) | `test_component_name_method_call_injection_is_rejected`, `test_component_name_semicolon_injection_is_rejected` | PASSING (2) -- mitigated |
|
|
683
|
-
| **HIGH-5** (slot name injection) | `test_slot_name_semicolon_injection_is_rejected`, `test_slot_name_with_parens_is_rejected` | PASSING (2) -- mitigated |
|
|
684
|
-
| **MEDIUM-1** (brace nesting DoS) | `test_deeply_nested_braces_in_expression_raises_error`, `test_deeply_nested_braces_in_attribute_raises_error` | PASSING (2) -- mitigated |
|
|
685
|
-
| **MEDIUM-3** (runtime_error breakout) | `test_runtime_error_with_bracket_produces_valid_ruby`, `test_runtime_error_code_injection_is_rejected` | PASSING (2) -- mitigated |
|
|
686
|
-
| **MEDIUM-4** (attribute name injection) | `test_attribute_name_with_single_quote_is_rejected`, `test_attribute_name_with_backtick_is_rejected` | PASSING (2) -- mitigated |
|
|
687
|
-
| **LOW-1** (verbatim bypass) | `test_verbatim_mode_does_not_process_expressions`, `test_verbatim_mode_passes_html_through_raw` | PASSING (2) |
|
|
688
|
-
| **LOW-2** (comment delimiter) | `test_comment_content_terminates_at_closing_delimiter` | PASSING (1) |
|
|
689
|
-
| **LOW-3** (no size limit) | `test_large_template_has_size_limit` | PASSING (1) -- mitigated |
|
|
690
|
-
| **LOW-4** (ReDoS) | `test_block_regex_handles_pathological_input` | PASSING (1) |
|
|
691
|
-
|
|
692
|
-
Baseline tests (expected to always pass): `test_printing_expression_is_escaped_baseline`, `test_static_attribute_value_needs_no_runtime_escape`, `test_with_directive_normal_usage_compiles_correctly`, `test_dynamic_tag_normal_usage_compiles_correctly`, `test_component_name_dotted_namespace_compiles_correctly`, `test_slot_normal_usage_compiles_correctly`, `test_moderate_brace_nesting_works`, `test_runtime_error_normal_message_compiles_correctly`, `test_valid_attribute_names_compile_correctly`, `test_normal_sized_template_works`, `test_for_block_normal_usage_compiles_correctly`, `test_for_directive_normal_usage_compiles_correctly`.
|
|
693
|
-
|
|
694
|
-
**Totals: 37 tests, 0 failing, 37 passing (all mitigated).**
|
|
695
|
-
|
|
696
|
-
When mitigations are applied, each finding's failing tests should turn green while all baseline tests remain passing.
|
|
697
|
-
|
|
698
|
-
---
|
|
699
|
-
|
|
700
|
-
## Methodology
|
|
701
|
-
|
|
702
|
-
This review was conducted through manual source code analysis of all files in the compilation pipeline:
|
|
703
|
-
|
|
704
|
-
- `lib/orb/patterns.rb` -- Regex patterns (attack surface definition)
|
|
705
|
-
- `lib/orb/tokenizer2.rb` -- Lexical analysis (input parsing)
|
|
706
|
-
- `lib/orb/parser.rb` -- Syntax analysis (AST construction)
|
|
707
|
-
- `lib/orb/ast/*.rb` -- AST node definitions (data model)
|
|
708
|
-
- `lib/orb/temple/compiler.rb` -- AST to Temple IR (code generation)
|
|
709
|
-
- `lib/orb/temple/filters.rb` -- Temple filters (component/block handling)
|
|
710
|
-
- `lib/orb/temple/attributes_compiler.rb` -- Attribute compilation
|
|
711
|
-
- `lib/orb/temple/engine.rb` -- Temple pipeline configuration
|
|
712
|
-
- `lib/orb/rails_template.rb` -- Rails integration
|
|
713
|
-
- `lib/orb/render_context.rb` -- Template execution context
|
|
714
|
-
|
|
715
|
-
Reference CVEs and advisories for ERB, HAML, and SLIM were consulted to identify analogous vulnerability patterns. The review focused on the data flow from template source through tokenization, parsing, AST construction, code generation, and execution.
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
# Proposal: `:unwrap` Directive for Orb Templates
|
|
2
|
-
|
|
3
|
-
## Problem
|
|
4
|
-
|
|
5
|
-
When a component should conditionally wrap its children, Orb forces you to duplicate the entire child markup:
|
|
6
|
-
|
|
7
|
-
```html
|
|
8
|
-
<Tooltip
|
|
9
|
-
:if="{@is_last_method}"
|
|
10
|
-
description="This is the only active login method."
|
|
11
|
-
>
|
|
12
|
-
<Toggle>
|
|
13
|
-
<Toggle:option
|
|
14
|
-
label="On"
|
|
15
|
-
to="{path(enabled:"
|
|
16
|
-
true)}
|
|
17
|
-
active="{@enabled}"
|
|
18
|
-
method="patch"
|
|
19
|
-
tone="success"
|
|
20
|
-
/>
|
|
21
|
-
<Toggle:option
|
|
22
|
-
label="Off"
|
|
23
|
-
to="{path(enabled:"
|
|
24
|
-
false)}
|
|
25
|
-
active="{!@enabled}"
|
|
26
|
-
method="patch"
|
|
27
|
-
/>
|
|
28
|
-
</Toggle>
|
|
29
|
-
</Tooltip>
|
|
30
|
-
{#if !@is_last_method}
|
|
31
|
-
<Toggle>
|
|
32
|
-
<Toggle:option
|
|
33
|
-
label="On"
|
|
34
|
-
to="{path(enabled:"
|
|
35
|
-
true)}
|
|
36
|
-
active="{@enabled}"
|
|
37
|
-
method="patch"
|
|
38
|
-
tone="success"
|
|
39
|
-
/>
|
|
40
|
-
<Toggle:option
|
|
41
|
-
label="Off"
|
|
42
|
-
to="{path(enabled:"
|
|
43
|
-
false)}
|
|
44
|
-
active="{!@enabled}"
|
|
45
|
-
method="patch"
|
|
46
|
-
/>
|
|
47
|
-
</Toggle>
|
|
48
|
-
{/if}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
`:if` controls whether an element **and its children** render. There is no way to say "skip the wrapper but keep the children."
|
|
52
|
-
|
|
53
|
-
## Proposed Solution
|
|
54
|
-
|
|
55
|
-
A new `:unwrap` directive. When the condition is true, the element is stripped and only its children are rendered. When false, the element renders normally with its children inside.
|
|
56
|
-
|
|
57
|
-
```html
|
|
58
|
-
<Tooltip
|
|
59
|
-
description="This is the only active login method."
|
|
60
|
-
:unwrap="{!@is_last_method}"
|
|
61
|
-
>
|
|
62
|
-
<Toggle>
|
|
63
|
-
<Toggle:option
|
|
64
|
-
label="On"
|
|
65
|
-
to="{path(enabled:"
|
|
66
|
-
true)}
|
|
67
|
-
active="{@enabled}"
|
|
68
|
-
method="patch"
|
|
69
|
-
tone="success"
|
|
70
|
-
/>
|
|
71
|
-
<Toggle:option
|
|
72
|
-
label="Off"
|
|
73
|
-
to="{path(enabled:"
|
|
74
|
-
false)}
|
|
75
|
-
active="{!@enabled}"
|
|
76
|
-
method="patch"
|
|
77
|
-
disabled="{@is_last_method}"
|
|
78
|
-
/>
|
|
79
|
-
</Toggle>
|
|
80
|
-
</Tooltip>
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
One child block, zero duplication. The Tooltip renders when `@is_last_method` is true. When false, the Toggle renders directly.
|
|
84
|
-
|
|
85
|
-
## Semantics
|
|
86
|
-
|
|
87
|
-
`:unwrap` is a sibling to `:if`, operating on the same element but with different behavior:
|
|
88
|
-
|
|
89
|
-
| Directive | When `true` | When `false` |
|
|
90
|
-
| ---------------- | ------------------------- | ------------------------- |
|
|
91
|
-
| `:if={cond}` | Render element + children | Render **nothing** |
|
|
92
|
-
| `:unwrap={cond}` | Render **children only** | Render element + children |
|
|
93
|
-
|
|
94
|
-
The name "unwrap" describes exactly what happens: the wrapper is removed, the children are what's left.
|
|
95
|
-
|
|
96
|
-
### Interaction with `:if`
|
|
97
|
-
|
|
98
|
-
Both directives can coexist on the same element. `:if` is evaluated first (it already is in the compiler). If `:if` is false, nothing renders. If `:if` is true, `:unwrap` then decides whether the element itself is included:
|
|
99
|
-
|
|
100
|
-
```html
|
|
101
|
-
<Tooltip description="..." :if="{@show_section}" :unwrap="{!@needs_tooltip}">
|
|
102
|
-
<Badge label="Status" />
|
|
103
|
-
</Tooltip>
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
| `@show_section` | `@needs_tooltip` | Result |
|
|
107
|
-
| --------------- | ---------------- | ------------------------------ |
|
|
108
|
-
| `false` | (any) | Nothing |
|
|
109
|
-
| `true` | `true` | `<Tooltip>` wrapping `<Badge>` |
|
|
110
|
-
| `true` | `false` | `<Badge>` alone |
|
|
111
|
-
|
|
112
|
-
### Works on HTML elements too
|
|
113
|
-
|
|
114
|
-
`:unwrap` is not component-specific. It works on any element:
|
|
115
|
-
|
|
116
|
-
```html
|
|
117
|
-
<div class="wrapper" :unwrap="{@minimal}">
|
|
118
|
-
<span>Always here</span>
|
|
119
|
-
</div>
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## Implementation
|
|
123
|
-
|
|
124
|
-
The change touches three files in the Orb gem. No tokenizer or parser changes are needed — the existing directive parsing infrastructure handles `:unwrap` automatically.
|
|
125
|
-
|
|
126
|
-
### 1. `lib/orb/ast/tag_node.rb` — Recognize `:unwrap` as a compiler directive
|
|
127
|
-
|
|
128
|
-
The `compiler_directives?` method gates entry into directive processing. Rather than chaining `||` for each new directive, introduce a constant set that serves as the single source of truth for which directives the compiler handles:
|
|
129
|
-
|
|
130
|
-
```ruby
|
|
131
|
-
# Current (line 195-197):
|
|
132
|
-
def compiler_directives?
|
|
133
|
-
directives.any? { |k, _v| k == :if || k == :for }
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Proposed:
|
|
137
|
-
COMPILER_DIRECTIVES = %i[if unwrap for].freeze
|
|
138
|
-
|
|
139
|
-
def compiler_directives?
|
|
140
|
-
directives.any? { |k, _v| COMPILER_DIRECTIVES.include?(k) }
|
|
141
|
-
end
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
The constant's ordering mirrors evaluation priority (`:if` first, `:for` last) and makes it trivial to add future directives without touching the method body.
|
|
145
|
-
|
|
146
|
-
### 2. `lib/orb/temple/compiler.rb` — Compile `:unwrap` into a Temple expression
|
|
147
|
-
|
|
148
|
-
Refactor `transform_directives_for_tag_node` into focused handler methods. The entry point becomes a clean pipeline that shows evaluation order at a glance, while each handler encapsulates the fetch-remove-transform logic for its directive:
|
|
149
|
-
|
|
150
|
-
```ruby
|
|
151
|
-
# Current (lines 157-176):
|
|
152
|
-
def transform_directives_for_tag_node(node)
|
|
153
|
-
# First, process any :if directives
|
|
154
|
-
if_directive = node.directives.fetch(:if, false)
|
|
155
|
-
if if_directive
|
|
156
|
-
node.remove_directive(:if)
|
|
157
|
-
return [:if,
|
|
158
|
-
if_directive,
|
|
159
|
-
transform(node)]
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Second, process any :for directives
|
|
163
|
-
for_directive = node.directives.fetch(:for, false)
|
|
164
|
-
if for_directive
|
|
165
|
-
node.remove_directive(:for)
|
|
166
|
-
return [:orb, :for, for_directive, transform(node)]
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
# Last, render as a dynamic node expression
|
|
170
|
-
transform(node)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Proposed:
|
|
174
|
-
def transform_directives_for_tag_node(node)
|
|
175
|
-
handle_if(node) || handle_unwrap(node) || handle_for(node) || transform(node)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
private
|
|
179
|
-
|
|
180
|
-
def handle_if(node)
|
|
181
|
-
directive = node.directives.fetch(:if, false)
|
|
182
|
-
return unless directive
|
|
183
|
-
|
|
184
|
-
node.remove_directive(:if)
|
|
185
|
-
[:if, directive, transform(node)]
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
def handle_unwrap(node)
|
|
189
|
-
directive = node.directives.fetch(:unwrap, false)
|
|
190
|
-
return unless directive
|
|
191
|
-
|
|
192
|
-
node.remove_directive(:unwrap)
|
|
193
|
-
[:orb, :unwrap, directive, transform(node), transform_children(node)]
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def handle_for(node)
|
|
197
|
-
directive = node.directives.fetch(:for, false)
|
|
198
|
-
return unless directive
|
|
199
|
-
|
|
200
|
-
node.remove_directive(:for)
|
|
201
|
-
[:orb, :for, directive, transform(node)]
|
|
202
|
-
end
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
The entry point reads as a priority chain: `:if` is evaluated first (can suppress everything), `:unwrap` second (can strip the wrapper), `:for` last (iterates). Each handler returns `nil` when its directive is absent, letting the chain fall through.
|
|
206
|
-
|
|
207
|
-
Note: the `:unwrap` handler carries **both** the full element (`transform(node)`) and the bare children (`transform_children(node)`). This gives the filter both branches without re-parsing.
|
|
208
|
-
|
|
209
|
-
### 3. `lib/orb/temple/filters.rb` — Generate the conditional output
|
|
210
|
-
|
|
211
|
-
Add a handler for the `:unwrap` expression:
|
|
212
|
-
|
|
213
|
-
```ruby
|
|
214
|
-
# Add after on_orb_if (around line 128):
|
|
215
|
-
|
|
216
|
-
# Handle an unwrap directive expression `[:orb, :unwrap, condition, element, children]`
|
|
217
|
-
#
|
|
218
|
-
# When condition is true, renders only the children (unwrapped).
|
|
219
|
-
# When condition is false, renders the full element with children inside.
|
|
220
|
-
#
|
|
221
|
-
# @param [String] condition The condition to be evaluated
|
|
222
|
-
# @param [Array] element The full element Temple expression
|
|
223
|
-
# @param [Array] children The children-only Temple expression
|
|
224
|
-
# @return [Array] compiled Temple expression
|
|
225
|
-
def on_orb_unwrap(condition, element, children)
|
|
226
|
-
[:if, condition, compile(children), compile(element)]
|
|
227
|
-
end
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
This generates:
|
|
231
|
-
|
|
232
|
-
```ruby
|
|
233
|
-
if condition
|
|
234
|
-
# children only (unwrapped)
|
|
235
|
-
else
|
|
236
|
-
# full element with children
|
|
237
|
-
end
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
## Compiled Output Example
|
|
241
|
-
|
|
242
|
-
Given:
|
|
243
|
-
|
|
244
|
-
```html
|
|
245
|
-
<Tooltip description="Explanation" :unwrap="{!@is_last_method}">
|
|
246
|
-
<Badge label="Status" />
|
|
247
|
-
</Tooltip>
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
The compiler produces:
|
|
251
|
-
|
|
252
|
-
```ruby
|
|
253
|
-
if !@is_last_method
|
|
254
|
-
# Children only — Badge renders directly
|
|
255
|
-
_v1 = render(Sirius2::Badge.new(label: "Status")) { }
|
|
256
|
-
_v1
|
|
257
|
-
else
|
|
258
|
-
# Full Tooltip wrapping Badge
|
|
259
|
-
_v2 = render(Sirius2::Tooltip.new(description: "Explanation")) do |__orb__tooltip|
|
|
260
|
-
_v1 = render(Sirius2::Badge.new(label: "Status")) { }
|
|
261
|
-
_v1
|
|
262
|
-
end
|
|
263
|
-
_v2
|
|
264
|
-
end
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
The children markup appears twice in compiled output (one per branch), but only one branch executes at runtime. This is the same strategy `:if`/`else` uses in any compiled template language.
|
|
268
|
-
|
|
269
|
-
## Security Analysis
|
|
270
|
-
|
|
271
|
-
`:unwrap` introduces no new input surface, no new interpolation sites, and no new runtime primitives. Here's why, examined from an attacker's perspective.
|
|
272
|
-
|
|
273
|
-
### Threat: Code injection via the condition expression
|
|
274
|
-
|
|
275
|
-
The `:unwrap={expression}` value is a Ruby expression embedded in the template, identical to `:if={expression}`. It follows the same code path: the tokenizer extracts the attribute value, the compiler emits it as the condition in an `if` statement, and Temple's `ControlFlow` filter generates the final Ruby code.
|
|
276
|
-
|
|
277
|
-
**No new risk.** If an attacker can control the `:unwrap` condition, they can equally control any `:if` condition or `{{ }}` expression. The trust boundary is the template source itself, which is developer-authored and not user-supplied.
|
|
278
|
-
|
|
279
|
-
### Threat: Bypassing security-critical wrappers
|
|
280
|
-
|
|
281
|
-
A wrapper element might enforce a security invariant — e.g., a `<SanitizedContainer>` that escapes its children, or a CSRF token wrapper. If `:unwrap` is applied to such an element, the children would render without the security boundary.
|
|
282
|
-
|
|
283
|
-
**Same risk as `:if`.** An `:if={false}` on the same element already removes it and all its children entirely. `:unwrap` is strictly less dangerous — the children still render, they just lose the wrapper. Both are template-author decisions, not runtime user input. If a developer writes `:unwrap` on a security-critical wrapper, that's a logic bug in the template, not a vulnerability in the directive.
|
|
284
|
-
|
|
285
|
-
### Threat: Double-rendering or state mutation from duplicated children
|
|
286
|
-
|
|
287
|
-
The compiled output contains the children in both branches of the `if`/`else`. Could an attacker trigger double execution?
|
|
288
|
-
|
|
289
|
-
**No.** Standard `if`/`else` control flow guarantees mutual exclusion — exactly one branch executes per render. The duplication is in the compiled template source, not in runtime execution. This is the same pattern every compiled template language uses for conditionals.
|
|
290
|
-
|
|
291
|
-
### Threat: XSS via unwrapped content
|
|
292
|
-
|
|
293
|
-
When unwrapped, children are emitted directly into the parent context. Could this bypass output escaping?
|
|
294
|
-
|
|
295
|
-
**No.** Escaping in Orb is applied per-expression at the `{{ }}` or component render boundary, not by wrapper elements. Removing a wrapper doesn't change how child expressions are escaped. A `<Badge label={user_input} />` is escaped identically whether it's inside a `<Tooltip>` or rendered bare.
|
|
296
|
-
|
|
297
|
-
### Threat: Interaction with `:if` creating unexpected states
|
|
298
|
-
|
|
299
|
-
When both `:if` and `:unwrap` are present, `:if` is processed first (it's higher in the directive priority chain). The resulting structure is:
|
|
300
|
-
|
|
301
|
-
```ruby
|
|
302
|
-
if if_condition
|
|
303
|
-
if unwrap_condition
|
|
304
|
-
children
|
|
305
|
-
else
|
|
306
|
-
element_with_children
|
|
307
|
-
end
|
|
308
|
-
end
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
**No edge case.** The nesting is straightforward. When `:if` is false, nothing renders — `:unwrap` is never evaluated. No partial-render or inconsistent-state scenario exists.
|
|
312
|
-
|
|
313
|
-
### Summary
|
|
314
|
-
|
|
315
|
-
`:unwrap` reuses the exact same compilation and escaping pipeline as `:if`. It introduces no new interpolation site, no new runtime eval, no new trust boundary. The only novel behavior is _which subtree is selected_ for rendering — and both subtrees were already present in the template, authored by the developer.
|
|
316
|
-
|
|
317
|
-
## Summary
|
|
318
|
-
|
|
319
|
-
- **3 files changed**, ~25 lines of code added (including refactoring existing directive handling)
|
|
320
|
-
- **0 tokenizer/parser changes** — existing directive infrastructure handles it
|
|
321
|
-
- Refactors `compiler_directives?` to use a constant set and `transform_directives_for_tag_node` into per-directive handler methods — both existing directives benefit from the cleaner structure
|
|
322
|
-
- Works on HTML elements and components equally
|
|
323
|
-
- Composes cleanly with `:if` (evaluated first) and `:for`
|