Skip to content

Commit e5c8e86

Browse files
authored
Merge pull request #9039 from romayalon/romy-cors-allowed-header-case-insensitive
CORS | Allowed headers regexp check should be case insensitive
2 parents 0557aa1 + 91983e1 commit e5c8e86

File tree

2 files changed

+166
-13
lines changed

2 files changed

+166
-13
lines changed

src/test/unit_tests/test_s3_ops.js

Lines changed: 165 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -484,20 +484,25 @@ mocha.describe('s3_ops', function() {
484484
});
485485

486486
mocha.describe('bucket-cors', function() {
487+
const cors_bucket_name = 'cors-bucket';
488+
const example_origin = 'http://www.example.com';
489+
const allowed_method = 'HEAD';
490+
const allowed_header = 'x-lower-case-header';
491+
const expose_header = 'Content-Disposition';
487492

488493
mocha.before(async function() {
489-
await s3.createBucket({ Bucket: "cors-bucket" });
494+
await s3.createBucket({ Bucket: cors_bucket_name});
490495
});
491496

492497
mocha.it('should put and get bucket cors with ID', async function() {
493498

494499
// put bucket cors
495500
const params = {
496-
Bucket: "cors-bucket",
501+
Bucket: cors_bucket_name,
497502
CORSConfiguration: {
498503
CORSRules: [{
499504
ID: 'rule1',
500-
AllowedOrigins: ["http://www.example.com"],
505+
AllowedOrigins: [example_origin],
501506
AllowedHeaders: ["*"],
502507
AllowedMethods: ["PUT", "POST", "DELETE"],
503508
ExposeHeaders: ["x-amz-server-side-encryption"]
@@ -507,18 +512,18 @@ mocha.describe('s3_ops', function() {
507512
await s3.putBucketCors(params);
508513

509514
// get bucket CORS
510-
const res = await s3.getBucketCors({ Bucket: "cors-bucket" });
515+
const res = await s3.getBucketCors({ Bucket: cors_bucket_name });
511516
assert.deepEqual(res.CORSRules, params.CORSConfiguration.CORSRules);
512517
});
513518

514519
mocha.it('should put and get bucket cors with max age seconds', async function() {
515520

516521
// put bucket cors
517522
const params = {
518-
Bucket: "cors-bucket",
523+
Bucket: cors_bucket_name,
519524
CORSConfiguration: {
520525
CORSRules: [{
521-
AllowedOrigins: ["http://www.example.com"],
526+
AllowedOrigins: [example_origin],
522527
AllowedMethods: ["PUT", "POST", "DELETE"],
523528
MaxAgeSeconds: 1500,
524529
}]
@@ -527,17 +532,17 @@ mocha.describe('s3_ops', function() {
527532
await s3.putBucketCors(params);
528533

529534
// get bucket CORS
530-
const res = await s3.getBucketCors({ Bucket: "cors-bucket" });
535+
const res = await s3.getBucketCors({ Bucket: cors_bucket_name });
531536
assert.deepEqual(res.CORSRules, params.CORSConfiguration.CORSRules);
532537
});
533538

534539
mocha.it('should fail on unsupported AllowedMethods', async function() {
535540
const unsupported_method = "JACKY";
536541
const params = {
537-
Bucket: "cors-bucket",
542+
Bucket: cors_bucket_name,
538543
CORSConfiguration: {
539544
CORSRules: [{
540-
AllowedOrigins: ["http://www.example.com"],
545+
AllowedOrigins: [example_origin],
541546
AllowedMethods: ["PUT", "POST", unsupported_method, "DELETE"],
542547
MaxAgeSeconds: 1500,
543548
}]
@@ -557,10 +562,10 @@ mocha.describe('s3_ops', function() {
557562
mocha.it('should fail on wildcar in ExposeHeader', async function() {
558563
const wildcard_expose_header = "x-amz-server-side-*";
559564
const params = {
560-
Bucket: "cors-bucket",
565+
Bucket: cors_bucket_name,
561566
CORSConfiguration: {
562567
CORSRules: [{
563-
AllowedOrigins: ["http://www.example.com"],
568+
AllowedOrigins: [example_origin],
564569
AllowedMethods: ["PUT", "POST", "DELETE"],
565570
ExposeHeaders: ["Content-Length", wildcard_expose_header]
566571
}]
@@ -577,8 +582,156 @@ mocha.describe('s3_ops', function() {
577582
}
578583
});
579584

585+
mocha.it('should put bucket cors with lower case header, no header in req', async function() {
586+
// put bucket cors
587+
const params = {
588+
Bucket: cors_bucket_name,
589+
CORSConfiguration: {
590+
CORSRules: [{
591+
ID: 'rule1',
592+
AllowedOrigins: [example_origin],
593+
AllowedHeaders: [allowed_header],
594+
AllowedMethods: [allowed_method],
595+
ExposeHeaders: [expose_header]
596+
}]
597+
}
598+
};
599+
await s3.putBucketCors(params);
600+
601+
// get bucket CORS
602+
const res = await s3.getBucketCors({ Bucket: cors_bucket_name });
603+
assert.deepEqual(res.CORSRules, params.CORSConfiguration.CORSRules);
604+
605+
// make an OPTIONS request without the allowed header
606+
const url = new URL(coretest.get_https_address());
607+
console.log('ROMY DEBUG: url', url, url.hostname, url.port, url.pathname);
608+
const response = await http_utils.make_https_request({
609+
hostname: url.hostname,
610+
port: url.port,
611+
path: `/${cors_bucket_name}/`,
612+
method: 'OPTIONS',
613+
rejectUnauthorized: false,
614+
headers: {
615+
'Access-Control-Request-Method': allowed_method,
616+
'Origin': example_origin,
617+
}
618+
});
619+
assert.deepEqual(response && response.statusCode, 200);
620+
});
621+
622+
mocha.it('should put bucket cors with lower case header, HEAD origin with upper case header', async function() {
623+
// put bucket cors
624+
const params = {
625+
Bucket: cors_bucket_name,
626+
CORSConfiguration: {
627+
CORSRules: [{
628+
ID: 'rule1',
629+
AllowedOrigins: [example_origin],
630+
AllowedHeaders: [allowed_header],
631+
AllowedMethods: [allowed_method],
632+
ExposeHeaders: [expose_header]
633+
}]
634+
}
635+
};
636+
await s3.putBucketCors(params);
637+
638+
// get bucket CORS
639+
const res = await s3.getBucketCors({ Bucket: cors_bucket_name });
640+
assert.deepEqual(res.CORSRules, params.CORSConfiguration.CORSRules);
641+
642+
// make an OPTIONS request with the allowed header in lower case
643+
const url = new URL(coretest.get_https_address());
644+
const response = await http_utils.make_https_request({
645+
hostname: url.hostname,
646+
port: url.port,
647+
path: `/${cors_bucket_name}/`,
648+
method: 'OPTIONS',
649+
rejectUnauthorized: false,
650+
headers: {
651+
'Access-Control-Request-Headers': allowed_header,
652+
'Access-Control-Request-Method': allowed_method,
653+
'Origin': example_origin,
654+
}
655+
});
656+
assert.deepEqual(response && response.statusCode, 200);
657+
});
658+
659+
mocha.it('should put bucket cors with lower case header, HEAD origin with upper case header', async function() {
660+
// put bucket cors
661+
const params = {
662+
Bucket: cors_bucket_name,
663+
CORSConfiguration: {
664+
CORSRules: [{
665+
ID: 'rule1',
666+
AllowedOrigins: [example_origin],
667+
AllowedHeaders: [allowed_header],
668+
AllowedMethods: [allowed_method],
669+
ExposeHeaders: [expose_header]
670+
}]
671+
}
672+
};
673+
await s3.putBucketCors(params);
674+
675+
// get bucket CORS
676+
const res = await s3.getBucketCors({ Bucket: cors_bucket_name });
677+
assert.deepEqual(res.CORSRules, params.CORSConfiguration.CORSRules);
678+
679+
// make an OPTIONS request with the allowed header in upper case
680+
const url = new URL(coretest.get_https_address());
681+
const response = await http_utils.make_https_request({
682+
hostname: url.hostname,
683+
port: url.port,
684+
path: `/${cors_bucket_name}/`,
685+
method: 'OPTIONS',
686+
rejectUnauthorized: false,
687+
headers: {
688+
'Access-Control-Request-Headers': allowed_header.toUpperCase(),
689+
'Access-Control-Request-Method': allowed_method,
690+
'Origin': example_origin,
691+
}
692+
});
693+
assert.deepEqual(response && response.statusCode, 200);
694+
});
695+
696+
mocha.it('should put bucket cors with lower case header, HEAD origin with invalid header', async function() {
697+
// put bucket cors
698+
const params = {
699+
Bucket: cors_bucket_name,
700+
CORSConfiguration: {
701+
CORSRules: [{
702+
ID: 'rule1',
703+
AllowedOrigins: [example_origin],
704+
AllowedHeaders: [allowed_header],
705+
AllowedMethods: [allowed_method],
706+
ExposeHeaders: [expose_header]
707+
}]
708+
}
709+
};
710+
await s3.putBucketCors(params);
711+
712+
// get bucket CORS
713+
const res = await s3.getBucketCors({ Bucket: cors_bucket_name });
714+
assert.deepEqual(res.CORSRules, params.CORSConfiguration.CORSRules);
715+
716+
// make an OPTIONS request with the allowed header in upper case
717+
const url = new URL(coretest.get_https_address());
718+
const response = await http_utils.make_https_request({
719+
hostname: url.hostname,
720+
port: url.port,
721+
path: `/${cors_bucket_name}/`,
722+
method: 'OPTIONS',
723+
rejectUnauthorized: false,
724+
headers: {
725+
'Access-Control-Request-Headers': 'X-Invalid-Header',
726+
'Access-Control-Request-Method': allowed_method,
727+
'Origin': example_origin,
728+
}
729+
});
730+
assert.deepEqual(response && response.statusCode, 403);
731+
});
732+
580733
mocha.after(async function() {
581-
await s3.deleteBucket({ Bucket: "cors-bucket" });
734+
await s3.deleteBucket({ Bucket: cors_bucket_name });
582735
});
583736
});
584737

src/util/http_utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ function set_cors_headers_s3(req, res, cors_rules) {
731731
const matched_rule = req.headers.origin && ( // find the first rule with origin and method match
732732
cors_rules.find(rule => {
733733
const allowed_origins_regex = rule.allowed_origins.map(r => RegExp(`^${r.replace(/\*/g, '.*')}$`));
734-
const allowed_headers_regex = rule.allowed_headers?.map(r => RegExp(`^${r.replace(/\*/g, '.*')}$`));
734+
const allowed_headers_regex = rule.allowed_headers?.map(r => RegExp(`^${r.replace(/\*/g, '.*')}$`, 'i'));
735735
return allowed_origins_regex.some(r => r.test(match_origin)) &&
736736
rule.allowed_methods.includes(match_method) &&
737737
// we can match if no request headers or if reuqest headers match the rule allowed headers

0 commit comments

Comments
 (0)