Skip to content

Commit d01c175

Browse files
initial commit
0 parents  commit d01c175

9 files changed

Lines changed: 948 additions & 0 deletions

File tree

LICENSE.md

Lines changed: 246 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Security Headers Plugin for OJS
2+
3+
This plugin enhances the security of your OJS installation by adding a comprehensive set of modern HTTP security headers. It operates with "**default with override**" logic, providing robust security out-of-the-box while allowing customization for specific needs.
4+
This plugin is developed and maintained by Ashvisual Theme. <a href="https://demo-ojs.ashvisual.com" target="_blank">See our professional OJS themes in action on our live demo</a>.
5+
6+
## Key Features
7+
8+
- 🛡️ **Adds Modern Security Headers:** Implements best practices to protect your site from common attacks like clickjacking, XSS, and MIME-sniffing.
9+
- ⚙️ **Secure Defaults:** Comes with recommended default values for all headers, so you don't need to configure anything unless required.
10+
- ✍️ **Flexible Customization:** Allows administrators to override the value of each header for the entire site or for individual journals.
11+
- 🗑️ **Removes `X-Powered-By` Header:** Hides server technology information to reduce the informational footprint.
12+
13+
### Managed Headers
14+
15+
This plugin manages the following headers:
16+
17+
- `X-Frame-Options`
18+
- `X-Content-Type-Options`
19+
- `X-XSS-Protection` (Included for compatibility, though deprecated)
20+
- `Content-Security-Policy`
21+
- `Cross-Origin-Embedder-Policy`
22+
- `Cross-Origin-Opener-Policy`
23+
- `Cross-Origin-Resource-Policy`
24+
- `Permissions-Policy`
25+
- `Referrer-Policy`
26+
- `Strict-Transport-Security`
27+
28+
---
29+
30+
## Requirements
31+
32+
- **OJS version:** 3.3.x and above.
33+
34+
---
35+
36+
## Installation
37+
38+
1. Download the latest release from the plugin's release page.
39+
2. Log in to your OJS dashboard as an Administrator.
40+
3. Navigate to **Website Settings > Plugins > Upload a New Plugin**.
41+
4. Upload the `.tar.gz` file you downloaded.
42+
5. Once installation is complete, enable the plugin from the **Installed Plugins** tab.
43+
44+
---
45+
46+
## Configuration
47+
48+
You can configure custom header values for your site or for a specific journal.
49+
50+
1. Navigate to **Website Settings > Plugins**.
51+
2. Find the **Security Headers by AshVisual Theme** plugin and click the **Settings** button.
52+
3. A modal window will appear with all configurable headers. The fields will be pre-filled with the secure default values.
53+
4. To change a header, edit its value in the corresponding field.
54+
5. To **disable** a specific header, clear its field so it is empty and then save. The other headers will remain active with their default or custom values.
55+
6. Click **Save**.
56+
57+
---
58+
59+
## Verifying the Headers
60+
61+
After configuring the plugin, you can verify that the security headers are being applied correctly using a free online tool.
62+
63+
1. Go to a site like [**securityheaders.com**](https://securityheaders.com).
64+
2. Enter the URL of your journal (e.g., `https://myjournal.com`).
65+
3. Initiate the scan.
66+
4. The results will show you which HTTP security headers are active on your site. You should see the headers enabled by this plugin listed in the report.
67+
68+
This will help you confirm that your configuration is working as expected.
69+
70+
---
71+
72+
### Default Values
73+
74+
If a header's value is not customized (or the settings have never been saved), the plugin applies the following secure default values:
75+
76+
| Header | Default Value |
77+
| :--------------------------- | :--------------------------------------------------------------------------------------------------------------------- |
78+
| X-Frame-Options | `SAMEORIGIN` |
79+
| X-Content-Type-Options | `nosniff` |
80+
| X-XSS-Protection | `1; mode=block` |
81+
| Content-Security-Policy | `upgrade-insecure-requests;` |
82+
| Cross-Origin-Embedder-Policy | `same-origin; report-to='default'` |
83+
| Cross-Origin-Opener-Policy | `require-corp` |
84+
| Cross-Origin-Resource-Policy | `same-origin` |
85+
| Permissions-Policy | `accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), usb=(), fullscreen=(self)` |
86+
| Referrer-Policy | `strict-origin-when-cross-origin` |
87+
| Strict-Transport-Security | `max-age=63072000; includeSubDomains; preload` |
88+
89+
---
90+
91+
## License
92+
93+
This plugin is released under the GNU General Public License v3. See the `LICENSE.md` or `docs/COPYING` file for full terms.

SecurityHeadersPlugin.inc.php

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
/**
4+
* @file plugins/generic/ashSecurityHeaders/SecurityHeadersPlugin.inc.php
5+
*
6+
* Copyright (c) 2021-2025 AshVisualTheme
7+
* Copyright (c) 2014-2025 Simon Fraser University
8+
* Copyright (c) 2003-2025 John Willinsky
9+
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
10+
*
11+
* @class SecurityHeadersPlugin
12+
* @brief Main class for the Security Headers plugin.
13+
*/
14+
15+
import('lib.pkp.classes.core.PKPApplication');
16+
import('lib.pkp.classes.core.JSONMessage');
17+
import('lib.pkp.classes.linkAction.LinkAction');
18+
import('lib.pkp.classes.linkAction.request.AjaxModal');
19+
import('lib.pkp.classes.plugins.GenericPlugin');
20+
import('lib.pkp.classes.plugins.HookRegistry');
21+
22+
import('plugins.generic.ashSecurityHeaders.SecurityHeadersSettingsForm');
23+
24+
class SecurityHeadersPlugin extends GenericPlugin
25+
{
26+
public function register($category, $path, $mainContextId = null)
27+
{
28+
$success = parent::register($category, $path, $mainContextId);
29+
if ($success && $this->getEnabled()) {
30+
HookRegistry::register('Dispatcher::dispatch', [$this, 'addSecurityHeaders']);
31+
}
32+
return $success;
33+
}
34+
35+
public function getDisplayName()
36+
{
37+
return __('plugins.generic.ashSecurityHeaders.displayName');
38+
}
39+
40+
public function getDescription()
41+
{
42+
return __('plugins.generic.ashSecurityHeaders.description');
43+
}
44+
45+
public function isSitePlugin()
46+
{
47+
if (!$this->getRequest()->getContext()) {
48+
return true;
49+
}
50+
return false;
51+
}
52+
53+
public function getActions($request, $actionArgs)
54+
{
55+
$actions = parent::getActions($request, $actionArgs);
56+
if (!$this->getEnabled()) {
57+
return $actions;
58+
}
59+
60+
$router = $request->getRouter();
61+
$linkAction = new LinkAction(
62+
'settings',
63+
new AjaxModal(
64+
$router->url(
65+
$request,
66+
null,
67+
null,
68+
'manage',
69+
null,
70+
['verb' => 'settings', 'plugin' => $this->getName(), 'category' => 'generic']
71+
),
72+
$this->getDisplayName()
73+
),
74+
__('manager.plugins.settings'),
75+
null
76+
);
77+
array_unshift($actions, $linkAction);
78+
return $actions;
79+
}
80+
81+
public function manage($args, $request)
82+
{
83+
switch ($request->getUserVar('verb')) {
84+
case 'settings':
85+
$form = new SecurityHeadersSettingsForm($this);
86+
87+
if (!$request->getUserVar('save')) {
88+
$form->initData();
89+
return new JSONMessage(true, $form->fetch($request));
90+
}
91+
92+
$form->readInputData();
93+
if ($form->validate()) {
94+
$form->execute();
95+
return new JSONMessage(true);
96+
}
97+
}
98+
return parent::manage($args, $request);
99+
}
100+
101+
public function getDefaultHeaders()
102+
{
103+
return [
104+
'X-Frame-Options' => 'SAMEORIGIN',
105+
'X-Content-Type-Options' => 'nosniff',
106+
'X-XSS-Protection' => '1; mode=block',
107+
'Content-Security-Policy' => "upgrade-insecure-requests;",
108+
'Cross-Origin-Embedder-Policy' => "same-origin; report-to='default'",
109+
'Cross-Origin-Opener-Policy' => 'require-corp',
110+
'Cross-Origin-Resource-Policy' => 'same-origin',
111+
'Permissions-Policy' => "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), usb=(), fullscreen=(self)",
112+
'Referrer-Policy' => 'strict-origin-when-cross-origin',
113+
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
114+
];
115+
}
116+
117+
public function addSecurityHeaders($hookName, $params)
118+
{
119+
120+
if (defined('SESSION_DISABLE_INIT') || php_sapi_name() === 'cli' || headers_sent()) {
121+
return false;
122+
}
123+
124+
$defaultHeaders = $this->getDefaultHeaders();
125+
$request = PKPApplication::get()->getRequest();
126+
$context = $request->getContext();
127+
$contextId = $context ? $context->getId() : CONTEXT_SITE;
128+
129+
$settingMap = [
130+
'X-Frame-Options' => 'headerXfo',
131+
'X-Content-Type-Options' => 'headerXcto',
132+
'X-XSS-Protection' => 'headerXxss',
133+
'Content-Security-Policy' => 'headerCsp',
134+
'Cross-Origin-Embedder-Policy' => 'headerCoep',
135+
'Cross-Origin-Opener-Policy' => 'headerCoop',
136+
'Cross-Origin-Resource-Policy' => 'headerCorp',
137+
'Permissions-Policy' => 'headerPp',
138+
'Referrer-Policy' => 'headerRp',
139+
'Strict-Transport-Security' => 'headerHsts',
140+
];
141+
142+
$finalHeaders = [];
143+
foreach ($settingMap as $headerName => $settingKey) {
144+
$savedValue = $this->getSetting($contextId, $settingKey);
145+
146+
if ($savedValue === null) {
147+
if (isset($defaultHeaders[$headerName])) {
148+
$finalHeaders[$headerName] = $defaultHeaders[$headerName];
149+
}
150+
} elseif ($savedValue !== '') {
151+
$finalHeaders[$headerName] = $savedValue;
152+
}
153+
}
154+
155+
header_remove('X-Powered-By');
156+
157+
if (!empty($finalHeaders)) {
158+
foreach ($finalHeaders as $name => $value) {
159+
header("{$name}: {$value}");
160+
}
161+
}
162+
163+
return false;
164+
}
165+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
/**
4+
* @file plugins/generic/ashSecurityHeaders/SecurityHeadersSettingsForm.inc.php
5+
*
6+
* Copyright (c) 2021-2025 AshVisualTheme
7+
* Copyright (c) 2014-2025 Simon Fraser University
8+
* Copyright (c) 2003-2025 John Willinsky
9+
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
10+
*
11+
* @class SecurityHeadersSettingsForm
12+
* @brief Form for managing the Security Headers plugin settings.
13+
*/
14+
15+
import('lib.pkp.classes.core.PKPApplication');
16+
import('lib.pkp.classes.form.Form');
17+
import('lib.pkp.classes.form.validation.FormValidatorCSRF');
18+
import('lib.pkp.classes.form.validation.FormValidatorPost');
19+
import('lib.pkp.classes.notification.PKPNotification');
20+
import('lib.pkp.classes.notification.PKPNotificationManager');
21+
import('lib.pkp.classes.template.PKPTemplateManager');
22+
23+
class SecurityHeadersSettingsForm extends Form
24+
{
25+
public SecurityHeadersPlugin $plugin;
26+
private $settingKeys;
27+
28+
public function __construct(SecurityHeadersPlugin $plugin)
29+
{
30+
parent::__construct($plugin->getTemplateResource('settings.tpl'));
31+
$this->plugin = $plugin;
32+
33+
$this->settingKeys = [
34+
'headerXfo',
35+
'headerXcto',
36+
'headerXxss',
37+
'headerCsp',
38+
'headerCoep',
39+
'headerCoop',
40+
'headerCorp',
41+
'headerPp',
42+
'headerRp',
43+
'headerHsts'
44+
];
45+
46+
$this->addCheck(new FormValidatorPost($this));
47+
$this->addCheck(new FormValidatorCSRF($this));
48+
}
49+
50+
public function initData()
51+
{
52+
$context = PKPApplication::get()->getRequest()->getContext();
53+
$contextId = $context ? $context->getId() : CONTEXT_SITE;
54+
$defaultHeaders = $this->plugin->getDefaultHeaders();
55+
56+
$settingMap = [
57+
'headerXfo' => 'X-Frame-Options',
58+
'headerXcto' => 'X-Content-Type-Options',
59+
'headerXxss' => 'X-XSS-Protection',
60+
'headerCsp' => 'Content-Security-Policy',
61+
'headerCoep' => 'Cross-Origin-Embedder-Policy',
62+
'headerCoop' => 'Cross-Origin-Opener-Policy',
63+
'headerCorp' => 'Cross-Origin-Resource-Policy',
64+
'headerPp' => 'Permissions-Policy',
65+
'headerRp' => 'Referrer-Policy',
66+
'headerHsts' => 'Strict-Transport-Security',
67+
];
68+
69+
foreach ($this->settingKeys as $key) {
70+
$savedValue = $this->plugin->getSetting($contextId, $key);
71+
72+
if ($savedValue === null) {
73+
$headerName = $settingMap[$key] ?? null;
74+
if ($headerName && isset($defaultHeaders[$headerName])) {
75+
$this->setData($key, $defaultHeaders[$headerName]);
76+
}
77+
} else {
78+
$this->setData($key, $savedValue);
79+
}
80+
}
81+
82+
parent::initData();
83+
}
84+
85+
public function readInputData()
86+
{
87+
$this->readUserVars($this->settingKeys);
88+
parent::readInputData();
89+
}
90+
91+
public function fetch($request, $template = null, $display = false)
92+
{
93+
$templateMgr = PKPTemplateManager::getManager($request);
94+
$templateMgr->assign('pluginName', $this->plugin->getName());
95+
return parent::fetch($request, $template, $display);
96+
}
97+
98+
public function execute(...$functionArgs)
99+
{
100+
$context = PKPApplication::get()->getRequest()->getContext();
101+
$contextId = $context ? $context->getId() : CONTEXT_SITE;
102+
103+
foreach ($this->settingKeys as $key) {
104+
$value = $this->getData($key);
105+
$sanitizedValue = is_string($value) ? preg_replace('/[\r\n]/', '', $value) : $value;
106+
$this->plugin->updateSetting($contextId, $key, $sanitizedValue);
107+
}
108+
109+
$notificationMgr = new PKPNotificationManager();
110+
$notificationMgr->createTrivialNotification(
111+
PKPApplication::get()->getRequest()->getUser()->getId(),
112+
NOTIFICATION_TYPE_SUCCESS,
113+
['contents' => __('common.changesSaved')]
114+
);
115+
116+
return parent::execute(...$functionArgs);
117+
}
118+
}

index.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
/**
4+
* @file plugins/generic/securityHeaders/index.php
5+
*
6+
* Copyright (c) 2021-2025 AshVisualTheme
7+
* Copyright (c) 2014-2025 Simon Fraser University
8+
* Copyright (c) 2003-2025 John Willinsky
9+
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
10+
*
11+
* @ingroup plugins_generic_securityHeaders
12+
* @brief Wrapper for the security headers plugin.
13+
*
14+
*/
15+
16+
require_once('SecurityHeadersPlugin.inc.php');
17+
return new SecurityHeadersPlugin();

0 commit comments

Comments
 (0)