Skip to content

Commit 9ea348e

Browse files
committed
Merge branch 'development'
2 parents 5698fcf + a18ec73 commit 9ea348e

File tree

12 files changed

+206
-49
lines changed

12 files changed

+206
-49
lines changed

CONTRIBUTING.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@ If you discover a security vulnerability, please send an email to the developmen
4747

4848
Please make sure your code runs on the following CFML Engines:
4949

50+
## Prerequisites
51+
52+
- BoxLang 1+ (Preferred)
5053
- Lucee 5+
51-
- Adobe ColdFusion 2018+
54+
- Adobe 2023+
5255

5356
## Coding Styles & Formatting
5457

box.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name":"ColdBox Security",
3-
"version":"3.5.0",
3+
"version":"3.6.0",
44
"location":"https://downloads.ortussolutions.com/ortussolutions/coldbox-modules/cbsecurity/@build.version@/[email protected]@.zip",
55
"author":"Ortus Solutions.com <[email protected]>",
66
"slug":"cbsecurity",
@@ -28,7 +28,7 @@
2828
"cbcsrf":"^3.0.0"
2929
},
3030
"devDependencies":{
31-
"commandbox-boxlang":"*",
31+
"commandbox-boxlang":"*",
3232
"commandbox-cfformat":"*",
3333
"commandbox-docbox":"*"
3434
},
@@ -48,10 +48,13 @@
4848
"format:watch":"cfformat watch handlers/,interceptors/,models/,test-harness/tests/specs,ModuleConfig.cfc ./.cfformat.json",
4949
"format:check":"cfformat check handlers/,interceptors/,models/,test-harness/tests/specs,ModuleConfig.cfc",
5050
"install:dependencies":"install --force && cd test-harness && install --force",
51+
"start:boxlang":"server start [email protected]",
5152
"start:lucee":"server start [email protected]",
5253
"start:2023":"server start [email protected]",
54+
"stop:boxlang":"server stop [email protected]",
5355
"stop:lucee":"server stop [email protected]",
5456
"stop:2023":"server stop [email protected]",
57+
"logs:boxlang":"server log [email protected] --follow",
5558
"logs:lucee":"server log [email protected] --follow",
5659
"logs:2023":"server log [email protected] --follow"
5760
},

changelog.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
## [Unreleased]
1111

12+
### Security
13+
14+
- **CRITICAL**: Fixed open redirect vulnerability in `_securedURL` handling. The `saveSecuredUrl()` method now validates redirect URLs to ensure they belong to the same host as the current request, preventing attackers from crafting malicious URLs that redirect users to external sites after login. Added `isSafeRedirectUrl()` validation using `java.net.URI` to compare hosts.
15+
16+
### Fixed
17+
18+
- BOX-164 Allow Visualizer to show settings when firewall.logging not enabled
19+
- JWT Handler improperly returns a value causing it to skip ColdBox's RestHandler's response formatting logic. This results in the entire response object being returned rather than just invoking getDataPacket()
20+
1221
## [3.5.0] - 2025-10-17
1322

1423
### Added

handlers/Jwt.cfc

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ component extends="coldbox.system.RestHandler" {
1414
function refreshToken( event, rc, prc ){
1515
// If endpoint not enabled, just 404 it
1616
if ( !variables.jwtService.getSettings().jwt.enableRefreshEndpoint ) {
17-
return event
17+
event
1818
.getResponse()
1919
.setErrorMessage(
2020
"Refresh Token Endpoint Disabled",
2121
404,
2222
"Disabled"
2323
);
24+
return;
2425
}
2526

2627
try {
@@ -32,27 +33,31 @@ component extends="coldbox.system.RestHandler" {
3233
.setData( prc.newTokens )
3334
.addMessage( "Tokens refreshed! The passed in refresh token has been invalidated" );
3435
} catch ( RefreshTokensNotActive e ) {
35-
return event.getResponse().setErrorMessage( "Refresh Tokens Not Active", 404, "Disabled" );
36+
event.getResponse().setErrorMessage( "Refresh Tokens Not Active", 404, "Disabled" );
3637
} catch ( TokenNotFoundException e ) {
37-
return event
38+
event
3839
.getResponse()
3940
.setErrorMessage(
4041
"The refresh token was not passed via the header or the rc. Cannot refresh the unrefreshable!",
4142
400,
4243
"Missing refresh token"
4344
);
4445
} catch ( TokenInvalidException e ) {
45-
prc.response.setErrorMessage(
46-
"Invalid Token - #e.message#",
47-
401,
48-
"Invalid Token"
49-
);
46+
event
47+
.getResponse()
48+
.setErrorMessage(
49+
"Invalid Token - #e.message#",
50+
401,
51+
"Invalid Token"
52+
);
5053
} catch ( TokenExpiredException e ) {
51-
prc.response.setErrorMessage(
52-
"Token Expired - #e.message#",
53-
400,
54-
"Token Expired"
55-
);
54+
event
55+
.getResponse()
56+
.setErrorMessage(
57+
"Token Expired - #e.message#",
58+
400,
59+
"Token Expired"
60+
);
5661
}
5762
}
5863

handlers/Visualizer.cfc

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,23 @@ component extends="coldbox.system.RestHandler" {
2020
}
2121
// Settings the visualizer will visualize :)
2222
prc.settings = variables.settings;
23-
prc.logCounts = dbLogger.count();
24-
prc.actionsReport = dbLogger.getActionsReport();
25-
prc.blockTypesReport = dbLogger.getBlockTypesReport();
26-
prc.topOffendingPaths = dbLogger.getTopOffending( "path" );
27-
prc.topOffendingIps = dbLogger.getTopOffending( "ip" );
28-
prc.topOffendingHosts = dbLogger.getTopOffending( "host" );
29-
prc.topOffendingUserAgents = dbLogger.getTopOffending( "userAgent" );
30-
prc.topOffendingMethods = dbLogger.getTopOffending( "httpMethod" );
31-
prc.topOffendingUsers = dbLogger.getTopOffending( "userId" );
32-
prc.logs = dbLogger.getLatest(
33-
top : 50,
34-
action : rc.action ?: "",
35-
blockType: rc.blockType ?: "",
36-
userId : rc.userId ?: ""
37-
);
23+
if( prc.settings.firewall.logs.enabled ){
24+
prc.logCounts = dbLogger.count();
25+
prc.actionsReport = dbLogger.getActionsReport();
26+
prc.blockTypesReport = dbLogger.getBlockTypesReport();
27+
prc.topOffendingPaths = dbLogger.getTopOffending( "path" );
28+
prc.topOffendingIps = dbLogger.getTopOffending( "ip" );
29+
prc.topOffendingHosts = dbLogger.getTopOffending( "host" );
30+
prc.topOffendingUserAgents = dbLogger.getTopOffending( "userAgent" );
31+
prc.topOffendingMethods = dbLogger.getTopOffending( "httpMethod" );
32+
prc.topOffendingUsers = dbLogger.getTopOffending( "userId" );
33+
prc.logs = dbLogger.getLatest(
34+
top : 50,
35+
action : rc.action ?: "",
36+
blockType: rc.blockType ?: "",
37+
userId : rc.userId ?: ""
38+
);
39+
}
3840
// Show the visualizer
3941
event.setView( "home/index" );
4042
}

interceptors/Security.cfc

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,11 @@ component accessors="true" extends="coldbox.system.Interceptor" {
200200
/**
201201
* Listen to module loadings, so we can do module rule registrations
202202
*
203-
* @event
203+
* @event
204204
* @interceptData
205-
* @rc
206-
* @prc
207-
* @buffer
205+
* @rc
206+
* @prc
207+
* @buffer
208208
*/
209209
function postModuleLoad( event, interceptData, rc, prc, buffer ){
210210
// Is this a cbSecurity Module & not registered
@@ -223,11 +223,11 @@ component accessors="true" extends="coldbox.system.Interceptor" {
223223
/**
224224
* Listen to module unloadings, so we can do module rule cleanups
225225
*
226-
* @event
226+
* @event
227227
* @interceptData
228-
* @rc
229-
* @prc
230-
* @buffer
228+
* @rc
229+
* @prc
230+
* @buffer
231231
*/
232232
function postModuleUnload( event, interceptData, rc, prc, buffer ){
233233
// Is the module registered?
@@ -246,11 +246,11 @@ component accessors="true" extends="coldbox.system.Interceptor" {
246246
/**
247247
* Our firewall kicks in at preProcess
248248
*
249-
* @event
249+
* @event
250250
* @interceptData
251-
* @rc
252-
* @prc
253-
* @buffer
251+
* @rc
252+
* @prc
253+
* @buffer
254254
*/
255255
function preProcess( event, interceptData, rc, prc, buffer ){
256256
// Add SecureView() into the requestcontext
@@ -290,9 +290,9 @@ component accessors="true" extends="coldbox.system.Interceptor" {
290290
/**
291291
* Process handler annotation based security rules.
292292
*
293-
* @event
293+
* @event
294294
* @interceptData
295-
* @currentEvent
295+
* @currentEvent
296296
*/
297297
function processAnnotationRules(
298298
required event,
@@ -773,6 +773,8 @@ component accessors="true" extends="coldbox.system.Interceptor" {
773773

774774
/**
775775
* Flash the incoming secured Url so we can redirect to it or use it in the next request.
776+
* This method validates the URL to prevent open redirect vulnerabilities by ensuring
777+
* the URL belongs to the same host as the current request.
776778
*
777779
* @event The event object
778780
*/
@@ -784,11 +786,58 @@ component accessors="true" extends="coldbox.system.Interceptor" {
784786
translate : false
785787
);
786788

789+
// Validate the URL to prevent open redirect attacks
790+
if ( !isSafeRedirectUrl( securedURL, arguments.event ) ) {
791+
// If the URL is not safe, log a warning and use the home page instead
792+
if ( log.canWarn() ) {
793+
log.warn(
794+
"Potential open redirect attempt detected. Invalid secured URL: #securedURL#. Using home page instead.",
795+
{ "ip" : variables.cbSecurity.getRealIp(), "url" : securedURL }
796+
);
797+
}
798+
// Use the application's base URL instead
799+
securedURL = arguments.event.buildLink( to = "", translate = false );
800+
}
801+
787802
// Flash it and place it in RC as well
788803
flash.put( "_securedUrl", securedURL );
789804
arguments.event.setValue( "_securedUrl", securedURL );
790805
}
791806

807+
/**
808+
* Validates that a redirect URL is safe by ensuring it belongs to the same host
809+
* as the current request. This prevents open redirect vulnerabilities.
810+
*
811+
* @targetUrl The URL to validate
812+
* @event The request context
813+
*
814+
* @return True if the URL is safe to redirect to, false otherwise
815+
*/
816+
private boolean function isSafeRedirectUrl( required string targetUrl, required event ){
817+
try {
818+
// Parse the URL to validate
819+
var urlToValidate = createObject( "java", "java.net.URI" ).init( arguments.targetUrl );
820+
821+
// If the URL is relative (no host), it's safe
822+
if ( isNull( urlToValidate.getHost() ) || !len( urlToValidate.getHost() ) ) {
823+
return true;
824+
}
825+
826+
// Get the current request's host for comparison
827+
var currentHost = variables.cbSecurity.getRealHost();
828+
829+
// Compare hosts (case-insensitive)
830+
return compareNoCase( urlToValidate.getHost(), currentHost ) == 0;
831+
} catch ( any e ) {
832+
// If URL parsing fails, consider it unsafe
833+
log.warn(
834+
"Error parsing URL for redirect validation: #arguments.targetUrl# : #e.message#",
835+
e.detail
836+
);
837+
return false;
838+
}
839+
}
840+
792841
/**
793842
* Verifies that the current event is in a given pattern list
794843
*

readme.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ Apache License, Version 2.0.
3636

3737
## Requirements
3838

39+
- BoxLang 1+ (Preferred)
3940
- Lucee 5+
40-
- ColdFusion 2018+
41+
- Adobe 2023+
4142
- ColdBox 6+
4243
- ColdBox 7+ for delegates and basic auth support only
4344

@@ -339,6 +340,8 @@ Once the firewall has the results, and the user is **NOT** allowed access. Then
339340
- The request will be logged via LogBox
340341
- If the firewall database logs are enabled, then we will log it in our database logs
341342
- The current URL will be flashed as `_securedURL` so it can be used in relocations
343+
- **Security Note**: The URL is validated to ensure it belongs to the same host as the current request to prevent open redirect attacks
344+
- Only same-host URLs or relative URLs are allowed; external URLs will be replaced with the application home page
342345
- If using a rule, the rule will be stored in `prc` as `cbsecurity_matchedRule`
343346
- The validator results will be stored in `prc` as `cbsecurity_validatorResults`
344347
- If the type of invalidation is `authentication` the `cbSecurity_onInvalidAuthentication` interception will be announced

[email protected]

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"cfengine":"boxlang@1"
66
},
77
"web":{
8+
"host":"0.0.0.0",
89
"http":{
910
"port":"60299"
1011
},
@@ -21,7 +22,7 @@
2122
"javaVersion":"openjdk21_jre",
2223
"args":"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888"
2324
},
24-
"openBrowser": false,
25+
"openBrowser":false,
2526
"cfconfig":{
2627
"file":".cfconfig.json"
2728
},

test-harness/config/Coldbox.cfc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
// Firewall Validator
9797
//"validator" : "BasicAuthValidator@cbsecurity",
9898
"logs" : {
99-
enabled : true
99+
enabled : false
100100
},
101101
// The global security rules
102102
"rules" : [

test-harness/tests/specs/integration/JWTSpec.cfc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/root" {
145145
404,
146146
event.getResponse().getMessagesString()
147147
);
148+
149+
// Matches the ColdBox RestHandler default response format spec
150+
var jsonResponse = deserializeJSON( event.getRenderedContent() );
151+
expect( jsonResponse ).toHaveLength( 4 );
152+
expect( jsonResponse ).toHaveKey( "data" );
153+
expect( jsonResponse ).toHaveKey( "error" );
154+
expect( jsonResponse ).toHaveKey( "pagination" );
155+
expect( jsonResponse ).toHaveKey( "messages" );
156+
expect( jsonResponse.messages[ 1 ] ).toBe( event.getResponse().getMessagesString() );
148157
} );
149158
} );
150159
given( "An activated endpoint but no refresh tokens passed", function(){

0 commit comments

Comments
 (0)