-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add non-terminal Headers rule #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| <?php | ||
|
|
||
| namespace Utopia\WAF\Rules; | ||
|
|
||
| use Utopia\WAF\Rule; | ||
|
|
||
| /** | ||
| * Carries response headers to add when the rule matches. | ||
| * | ||
| * Unlike the other actions this rule is non-terminal: it does not decide the | ||
| * request, so the firewall records it and keeps evaluating the rules after it. | ||
| */ | ||
| class Headers extends Rule | ||
| { | ||
| /** | ||
| * @var array<string, string> | ||
| */ | ||
| private array $headers; | ||
|
|
||
| /** | ||
| * @param array<\Utopia\WAF\Condition|array<string, mixed>> $conditions | ||
| * @param array<string, string> $headers Response headers, keyed by header name. | ||
| */ | ||
| public function __construct(array $conditions = [], array $headers = []) | ||
| { | ||
| parent::__construct($conditions); | ||
|
|
||
| if ($headers === []) { | ||
| throw new \InvalidArgumentException('Headers rule requires at least one header.'); | ||
| } | ||
|
|
||
| foreach ($headers as $name => $value) { | ||
| if (!\is_string($name) || preg_match('/^[A-Za-z0-9!#$%&\'*+.^_`|~-]+$/', $name) !== 1) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/Rules/Headers.php
Line: 33
Comment:
**Numeric header names rejected** A numeric-only name such as `"123"` is a valid HTTP token, but PHP converts it to an integer array key before this check. `is_string($name)` therefore rejects it, preventing callers from registering that valid header name.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| throw new \InvalidArgumentException('Invalid header name: ' . $name); | ||
| } | ||
|
|
||
| // Control characters would let a value smuggle in further headers. | ||
| if (!\is_string($value) || preg_match('/[\x00-\x08\x0A-\x1F\x7F]/', $value) === 1) { | ||
| throw new \InvalidArgumentException('Invalid value for header: ' . $name); | ||
| } | ||
| } | ||
|
|
||
| $this->headers = $headers; | ||
| } | ||
|
|
||
| public function getAction(): string | ||
| { | ||
| return self::ACTION_HEADERS; | ||
| } | ||
|
|
||
| public function isTerminal(): bool | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * @return array<string, string> | ||
| */ | ||
| public function getHeaders(): array | ||
| { | ||
| return $this->headers; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |||||
| use Utopia\WAF\Rules\Bypass; | ||||||
| use Utopia\WAF\Rules\Challenge; | ||||||
| use Utopia\WAF\Rules\Deny; | ||||||
| use Utopia\WAF\Rules\Headers; | ||||||
| use Utopia\WAF\Rules\RateLimit; | ||||||
| use Utopia\WAF\Rules\Redirect; | ||||||
|
|
||||||
|
|
@@ -67,4 +68,35 @@ public function testRedirectRule(): void | |||||
| $this->assertSame('/new', $rule->getLocation()); | ||||||
| $this->assertSame(301, $rule->getStatusCode()); | ||||||
| } | ||||||
|
|
||||||
| public function testHeadersRule(): void | ||||||
| { | ||||||
| $rule = new Headers([ | ||||||
| Condition::startsWith('path', '/api'), | ||||||
| ], headers: ['X-Frame-Options' => 'DENY']); | ||||||
|
|
||||||
| $this->assertTrue($rule->matches(['path' => '/api/users'])); | ||||||
| $this->assertSame('headers', $rule->getAction()); | ||||||
| $this->assertSame(['X-Frame-Options' => 'DENY'], $rule->getHeaders()); | ||||||
| $this->assertFalse($rule->isTerminal()); | ||||||
| $this->assertTrue((new Deny())->isTerminal()); | ||||||
|
Comment on lines
+81
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Context Used: Call out and harshly judge implementation-coupled tests. We don't mirror source code, configuration, or version pins in assertions. We test observable behavior; use linters for syntax and schema checks. (source) Prompt To Fix With AIThis is a comment left during a code review.
Path: tests/RulesTest.php
Line: 81-82
Comment:
**Tests assert implementation flags** These assertions check `isTerminal()` directly, while the firewall tests already show the observable behavior: evaluation continues after a Headers match. The repository requires tests of observable behavior rather than assertions that mirror implementation choices. Remove these flag checks before merging.
```suggestion
```
**Context Used:** Call out and harshly judge implementation-coupled tests. We don't mirror source code, configuration, or version pins in assertions. We test observable behavior; use linters for syntax and schema checks. ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||||||
| } | ||||||
|
|
||||||
| public function testHeadersRuleRejectsEmptyHeaders(): void | ||||||
| { | ||||||
| $this->expectException(\InvalidArgumentException::class); | ||||||
| new Headers([], headers: []); | ||||||
| } | ||||||
|
|
||||||
| public function testHeadersRuleRejectsInvalidName(): void | ||||||
| { | ||||||
| $this->expectException(\InvalidArgumentException::class); | ||||||
| new Headers([], headers: ['X Frame: Options' => 'DENY']); | ||||||
| } | ||||||
|
|
||||||
| public function testHeadersRuleRejectsLineBreaksInValue(): void | ||||||
| { | ||||||
| $this->expectException(\InvalidArgumentException::class); | ||||||
| new Headers([], headers: ['X-Frame-Options' => "DENY\r\nSet-Cookie: session=1"]); | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"X-Test "passes this check because$can match just before a final newline. The rule then exposes a name that is not a valid HTTP token, so a consumer writing the collected headers may reject it or handle it unsafely. Anchor the match to the absolute end of the string.Prompt To Fix With AI