Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Model/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class Config extends PageCacheConfig

public const XML_PATH_VARNISH_PASS_ON_COOKIE_PRESENCE = 'system/full_page_cache/varnish/pass_on_cookie_presence';

public const XML_PATH_VARNISH_CUSTOM_VCL_PREPEND_FILE = 'system/full_page_cache/varnish/custom_vcl_prepend_file';

public const XML_PATH_VARNISH_CUSTOM_VCL_APPEND_FILE = 'system/full_page_cache/varnish/custom_vcl_append_file';

public function __construct(
ReadFactory $readFactory,
ScopeConfigInterface $scopeConfig,
Expand Down Expand Up @@ -132,4 +136,14 @@ public function getEnableStaticCache(): bool
{
return (bool) $this->scopeConfig->getValue(static::XML_PATH_VARNISH_ENABLE_STATIC_CACHE);
}

public function getCustomVclPrependFile(): string
{
return (string) $this->scopeConfig->getValue(static::XML_PATH_VARNISH_CUSTOM_VCL_PREPEND_FILE);
}

public function getCustomVclAppendFile(): string
{
return (string) $this->scopeConfig->getValue(static::XML_PATH_VARNISH_CUSTOM_VCL_APPEND_FILE);
}
}
100 changes: 99 additions & 1 deletion Model/Varnish/VCLGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@

class VCLGenerator extends \Magento\PageCache\Model\Varnish\VclGenerator
{
/**
* Paths to sensitive system directories that should never be accessible
*/
private const BLOCKED_PATHS = [
'/etc/passwd',
'/etc/shadow',
'/root',
'/etc/ssh',
'/proc',
'/sys',
];

/**
* Maximum file size for custom VCL files (1MB)
*/
private const MAX_FILE_SIZE = 1048576; // 1024 * 1024

/**
* @var array|null Cached resolved blocked paths
*/
private static ?array $resolvedBlockedPaths = null;

public function __construct(
private readonly TemplateFactory $templateFactory,
private readonly VclTemplateLocatorInterface $vclTemplateLocator,
Expand Down Expand Up @@ -54,7 +76,9 @@ public function getVariables(): array
'use_xkey_vmod' => (bool) $this->varnishExtendedConfig->getUseXkeyVmod(),
'use_soft_purging' => (bool) $this->varnishExtendedConfig->getUseSoftPurging(),
'pass_on_cookie_presence' => $this->varnishExtendedConfig->getPassOnCookiePresence(),
'design_exceptions_code' => $this->getRegexForDesignExceptions()
'design_exceptions_code' => $this->getRegexForDesignExceptions(),
'custom_vcl_prepend' => $this->getCustomVclContent($this->varnishExtendedConfig->getCustomVclPrependFile()),
'custom_vcl_append' => $this->getCustomVclContent($this->varnishExtendedConfig->getCustomVclAppendFile()),
];
}

Expand Down Expand Up @@ -107,4 +131,78 @@ private function getTransformedAccessList(): array
}
return $result;
}

/**
* Get custom VCL content from file
*
* @param string $filePath
* @return string
*/
private function getCustomVclContent(string $filePath): string
{
if (empty($filePath)) {
return '';
}

$realPath = realpath($filePath);
if ($realPath === false) {
return '';
}

// Security: Prevent access to sensitive system directories
// Use cached resolved paths for performance
if (self::$resolvedBlockedPaths === null) {
self::$resolvedBlockedPaths = [];
foreach (self::BLOCKED_PATHS as $blocked) {
$blockedReal = realpath($blocked);
if ($blockedReal !== false) {
self::$resolvedBlockedPaths[] = $blockedReal;
}
}
}

foreach (self::$resolvedBlockedPaths as $blockedReal) {
if ($this->isPathWithinBlockedDirectory($realPath, $blockedReal)) {
return '';
}
}

if (!is_readable($realPath)) {
return '';
}

// Security: Limit file size to prevent memory exhaustion
$fileSize = filesize($realPath);
if ($fileSize === false || $fileSize > self::MAX_FILE_SIZE) {
return '';
}

$content = file_get_contents($realPath);
if ($content === false) {
// Note: Silent failure is intentional for security reasons
// Administrators can check Varnish logs if VCL generation has issues
return '';
}

return $content;
}

/**
* Check if a path is within a blocked directory
*
* @param string $path The real path to check
* @param string $blockedPath The blocked directory path
* @return bool
*/
private function isPathWithinBlockedDirectory(string $path, string $blockedPath): bool
{
if (strpos($path, $blockedPath) !== 0) {
return false;
}

// Path must be exactly the blocked path or start with blocked path + separator
return $path === $blockedPath ||
(strlen($path) > strlen($blockedPath) &&
$path[strlen($blockedPath)] === DIRECTORY_SEPARATOR);
}
}
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,56 @@ task('vcl:auto-apply', function () {

Contrary to popular belief, loading & activating ('using') a new VCL does not purge the cache objects already in Varnish. However, the new VCL might change how future requests are processed, which could result in cached items being evicted sooner or fetched differently.

## Custom VCL Prepend & Append

You can include custom VCL snippets at the start and end of the generated VCL by configuring file paths in the admin panel:

**Stores > Configuration > System > Full Page Cache > Varnish Configuration**
- **Custom VCL Prepend File**: Absolute path to a VCL file to include after imports but before the backend definition
- **Custom VCL Append File**: Absolute path to a VCL file to include at the end of the VCL

### How VCL Subroutine Prepend/Append Works

- Only one definition per VCL hook subroutine (e.g., `vcl_deliver`) is active in the final compiled VCL. Later definitions override earlier ones.
- Use `return` to prevent fallthrough to builtin VCL logic.
- Use prepend/append files to structure reusable logic (e.g., define shared sub blocks), but call them explicitly in the active `vcl_*` subroutine.
- If composing VCLs via includes, make sure only one file defines a given `vcl_*` unless you understand the override order.
- Use `varnishd -C -f composed.vcl` to inspect what actually gets compiled.

### Example: Prepend File

```vcl
# prepend.vcl
# Define custom backends or ACLs before the main VCL logic

backend custom_api {
.host = "api.example.com";
.port = "8080";
}
```

### Example: Append File

```vcl
# append.vcl
# Define helper subroutines that can be called from the main VCL

sub custom_security_check {
if (req.http.X-Custom-Header) {
# Custom security logic
}
}
```

**Important Notes:**
- **Security**: File size is limited to 1MB to prevent memory issues. Access to common system directories (e.g., /etc/passwd, /root) is blocked.
- Store your custom VCL files in your application directory (e.g., `app/etc/prepend.vcl`)
- Avoid adding `return` statements in prepend files unless you want to override all core logic
- Prepend files are ideal for: custom backends, ACLs, global variables
- Append files are ideal for: helper subroutines, custom logic that can be called from main VCL
- Test your custom VCL thoroughly in a non-production environment first
- Use `varnishd -C -f your.vcl` to validate the compiled VCL before deploying

## Compatibility

Needs at least Magento 2.4.7 and Varnish 6.4.
Expand Down
14 changes: 14 additions & 0 deletions etc/adminhtml/system.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@
<field id="system/full_page_cache/varnish/use_xkey_vmod">1</field>
</depends>
</field>
<field id="custom_vcl_prepend_file" type="text" translate="label comment" sortOrder="37" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Custom VCL Prepend File</label>
<comment><![CDATA[Absolute path to a custom VCL file to include at the start of the generated VCL. This file will be included before the main VCL logic. <strong>Security:</strong> File size limited to 1MB. Access to common system directories is blocked. Use with caution - incorrect VCL can break your cache.]]></comment>
<depends>
<field id="caching_application">1</field>
</depends>
</field>
<field id="custom_vcl_append_file" type="text" translate="label comment" sortOrder="38" showInDefault="1" showInWebsite="0" showInStore="0">
<label>Custom VCL Append File</label>
<comment><![CDATA[Absolute path to a custom VCL file to include at the end of the generated VCL. This file will be included after the main VCL logic. <strong>Security:</strong> File size limited to 1MB. Access to common system directories is blocked. Use with caution - incorrect VCL can break your cache.]]></comment>
<depends>
<field id="caching_application">1</field>
</depends>
</field>
</group>
</group>
</section>
Expand Down
4 changes: 4 additions & 0 deletions etc/varnish6.vcl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import xkey;
# The minimal Varnish version is 6.0
# For SSL offloading, pass the following header in your proxy server or load balancer: '{{var ssl_offloaded_header }}: https'

{{var custom_vcl_prepend}}

backend default {
.host = "{{var host}}";
.port = "{{var port}}";
Expand Down Expand Up @@ -315,3 +317,5 @@ sub vcl_synth {
return(deliver);
}
}

{{var custom_vcl_append}}
64 changes: 64 additions & 0 deletions tests/varnish/custom_vcl_prepend_append.vtc
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
varnishtest "Test custom VCL prepend and append"

barrier b1 cond 2

server s1 {
# first request will be the probe, handle it and be on our way
rxreq
expect req.url == "/health_check.php"
txresp

# the probe expects the connection to close
close
barrier b1 sync
accept

rxreq
expect req.url == "/"
expect req.method == "GET"
txresp -hdr "Cache-Control: public, max-age=86400"
} -start

# Create prepend VCL file with just comments for verification
shell {
cat > "${tmpdir}/prepend.vcl" <<'EOF'
# Custom prepend VCL
# This demonstrates prepended VCL logic can be included

EOF
}

# Create append VCL file with just comments for verification
shell {
cat > "${tmpdir}/append.vcl" <<'EOF'

# Custom append VCL
# This demonstrates appended VCL logic can be included
EOF
}

# Generate the VCL file based on included variables and write it to output.vcl
shell {
export s1_addr="${s1_addr}"
export s1_port="${s1_port}"
export CUSTOM_VCL_PREPEND="$(cat ${tmpdir}/prepend.vcl)"
export CUSTOM_VCL_APPEND="$(cat ${tmpdir}/append.vcl)"
${testdir}/helpers/parse_vcl.pl "${testdir}/../../etc/varnish6.vcl" "${tmpdir}/output.vcl"
}

varnish v1 -arg "-f" -arg "${tmpdir}/output.vcl" -arg "-p" -arg "vsl_mask=+Hash" -start

# make sure the probe request fired
barrier b1 sync

# Verify that the generated VCL contains the prepend and append content
shell {
grep -q "Custom prepend VCL" "${tmpdir}/output.vcl" || exit 1
grep -q "Custom append VCL" "${tmpdir}/output.vcl" || exit 1
}

client c1 {
txreq -method "GET" -url "/"
rxresp
expect resp.status == 200
} -run