Skip to content

Commit f810265

Browse files
nija-atrix0rrr
authored andcommitted
fix(s3): Add validations for S3 bucket names (#2256)
Bucket names are verified to conform with rules published by S3 - https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html Fixes #1308
1 parent ec367c8 commit f810265

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-0
lines changed

packages/@aws-cdk/aws-s3/lib/bucket.ts

+38
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import iam = require('@aws-cdk/aws-iam');
33
import kms = require('@aws-cdk/aws-kms');
44
import { IBucketNotificationDestination } from '@aws-cdk/aws-s3-notifications';
55
import cdk = require('@aws-cdk/cdk');
6+
import { EOL } from 'os';
67
import { BucketPolicy } from './bucket-policy';
78
import { BucketNotifications } from './notifications-resource';
89
import perms = require('./perms');
@@ -669,6 +670,9 @@ export class Bucket extends BucketBase {
669670
super(scope, id);
670671

671672
const { bucketEncryption, encryptionKey } = this.parseEncryption(props);
673+
if (props.bucketName && !cdk.Token.unresolved(props.bucketName)) {
674+
this.validateBucketName(props.bucketName);
675+
}
672676

673677
const resource = new CfnBucket(this, 'Resource', {
674678
bucketName: props && props.bucketName,
@@ -776,6 +780,40 @@ export class Bucket extends BucketBase {
776780
return this.onEvent(EventType.ObjectRemoved, dest, ...filters);
777781
}
778782

783+
private validateBucketName(bucketName: string) {
784+
const errors: string[] = [];
785+
786+
// Rules codified from https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
787+
if (bucketName.length < 3 || bucketName.length > 63) {
788+
errors.push('Bucket name must be at least 3 and no more than 63 characters');
789+
}
790+
const charsetMatch = bucketName.match(/[^a-z0-9.-]/);
791+
if (charsetMatch) {
792+
errors.push('Bucket name must only contain lowercase characters and the symbols, period (.) and dash (-) '
793+
+ `(offset: ${charsetMatch.index})`);
794+
}
795+
if (!/[a-z0-9]/.test(bucketName.charAt(0))) {
796+
errors.push('Bucket name must start and end with a lowercase character or number '
797+
+ '(offset: 0)');
798+
}
799+
if (!/[a-z0-9]/.test(bucketName.charAt(bucketName.length - 1))) {
800+
errors.push('Bucket name must start and end with a lowercase character or number '
801+
+ `(offset: ${bucketName.length - 1})`);
802+
}
803+
const consecSymbolMatch = bucketName.match(/\.-|-\.|\.\./);
804+
if (consecSymbolMatch) {
805+
errors.push('Bucket name must not have dash next to period, or period next to dash, or consecutive periods '
806+
+ `(offset: ${consecSymbolMatch.index})`);
807+
}
808+
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(bucketName)) {
809+
errors.push('Bucket name must not resemble an IP address');
810+
}
811+
812+
if (errors.length > 0) {
813+
throw new Error(`Invalid S3 bucket name (value: ${bucketName})${EOL}${errors.join(EOL)}`);
814+
}
815+
}
816+
779817
/**
780818
* Set up key properties and return the Bucket encryption property from the
781819
* user's configuration.

packages/@aws-cdk/aws-s3/test/test.bucket.ts

+137
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import iam = require('@aws-cdk/aws-iam');
33
import kms = require('@aws-cdk/aws-kms');
44
import cdk = require('@aws-cdk/cdk');
55
import { Test } from 'nodeunit';
6+
import { EOL } from 'os';
67
import s3 = require('../lib');
78

89
// to make it easy to copy & paste from output:
@@ -72,6 +73,142 @@ export = {
7273
test.done();
7374
},
7475

76+
'valid bucket names'(test: Test) {
77+
const stack = new cdk.Stack();
78+
79+
test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket1', {
80+
bucketName: 'abc.xyz-34ab'
81+
}));
82+
83+
test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket2', {
84+
bucketName: '124.pp--33'
85+
}));
86+
87+
test.done();
88+
},
89+
90+
'bucket validation skips tokenized values'(test: Test) {
91+
const stack = new cdk.Stack();
92+
93+
test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket', {
94+
bucketName: new cdk.Token(() => '_BUCKET').toString()
95+
}));
96+
97+
test.done();
98+
},
99+
100+
'fails with message on invalid bucket names'(test: Test) {
101+
const stack = new cdk.Stack();
102+
const bucket = `-buckEt.-${new Array(65).join('$')}`;
103+
const expectedErrors = [
104+
`Invalid S3 bucket name (value: ${bucket})`,
105+
'Bucket name must be at least 3 and no more than 63 characters',
106+
'Bucket name must only contain lowercase characters and the symbols, period (.) and dash (-) (offset: 5)',
107+
'Bucket name must start and end with a lowercase character or number (offset: 0)',
108+
`Bucket name must start and end with a lowercase character or number (offset: ${bucket.length - 1})`,
109+
'Bucket name must not have dash next to period, or period next to dash, or consecutive periods (offset: 7)',
110+
].join(EOL);
111+
112+
test.throws(() => new s3.Bucket(stack, 'MyBucket', {
113+
bucketName: bucket
114+
// tslint:disable-next-line:only-arrow-functions
115+
}), function(err: Error) {
116+
return expectedErrors === err.message;
117+
});
118+
119+
test.done();
120+
},
121+
122+
'fails if bucket name has less than 3 or more than 63 characters'(test: Test) {
123+
const stack = new cdk.Stack();
124+
125+
test.throws(() => new s3.Bucket(stack, 'MyBucket1', {
126+
bucketName: 'a'
127+
}), /at least 3/);
128+
129+
test.throws(() => new s3.Bucket(stack, 'MyBucket2', {
130+
bucketName: new Array(65).join('x')
131+
}), /no more than 63/);
132+
133+
test.done();
134+
},
135+
136+
'fails if bucket name has invalid characters'(test: Test) {
137+
const stack = new cdk.Stack();
138+
139+
test.throws(() => new s3.Bucket(stack, 'MyBucket1', {
140+
bucketName: 'b@cket'
141+
}), /offset: 1/);
142+
143+
test.throws(() => new s3.Bucket(stack, 'MyBucket2', {
144+
bucketName: 'bucKet'
145+
}), /offset: 3/);
146+
147+
test.throws(() => new s3.Bucket(stack, 'MyBucket3', {
148+
bucketName: 'bučket'
149+
}), /offset: 2/);
150+
151+
test.done();
152+
},
153+
154+
'fails if bucket name does not start or end with lowercase character or number'(test: Test) {
155+
const stack = new cdk.Stack();
156+
157+
test.throws(() => new s3.Bucket(stack, 'MyBucket1', {
158+
bucketName: '-ucket'
159+
}), /offset: 0/);
160+
161+
test.throws(() => new s3.Bucket(stack, 'MyBucket2', {
162+
bucketName: 'bucke.'
163+
}), /offset: 5/);
164+
165+
test.done();
166+
},
167+
168+
'fails only if bucket name has the consecutive symbols (..), (.-), (-.)'(test: Test) {
169+
const stack = new cdk.Stack();
170+
171+
test.throws(() => new s3.Bucket(stack, 'MyBucket1', {
172+
bucketName: 'buc..ket'
173+
}), /offset: 3/);
174+
175+
test.throws(() => new s3.Bucket(stack, 'MyBucket2', {
176+
bucketName: 'buck.-et'
177+
}), /offset: 4/);
178+
179+
test.throws(() => new s3.Bucket(stack, 'MyBucket3', {
180+
bucketName: 'b-.ucket'
181+
}), /offset: 1/);
182+
183+
test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket4', {
184+
bucketName: 'bu--cket'
185+
}));
186+
187+
test.done();
188+
},
189+
190+
'fails only if bucket name resembles IP address'(test: Test) {
191+
const stack = new cdk.Stack();
192+
193+
test.throws(() => new s3.Bucket(stack, 'MyBucket1', {
194+
bucketName: '1.2.3.4'
195+
}), /must not resemble an IP address/);
196+
197+
test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket2', {
198+
bucketName: '1.2.3'
199+
}));
200+
201+
test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket3', {
202+
bucketName: '1.2.3.a'
203+
}));
204+
205+
test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket4', {
206+
bucketName: '1000.2.3.4'
207+
}));
208+
209+
test.done();
210+
},
211+
75212
'fails if encryption key is used with managed encryption'(test: Test) {
76213
const stack = new cdk.Stack();
77214
const myKey = new kms.EncryptionKey(stack, 'MyKey');

0 commit comments

Comments
 (0)