Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cognito): quote or mime-encode fromName to comply RFC 5322 #23227

Merged
merged 4 commits into from Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-cognito/README.md
Expand Up @@ -431,6 +431,18 @@ new cognito.UserPool(this, 'myuserpool', {
});
```

If `fromName` does not comply RFC 5322 atom or quoted-string, it will be quoted or mime-encoded.

```ts
new cognito.UserPool(this, 'myuserpool', {
email: cognito.UserPoolEmail.withSES({
fromEmail: 'noreply@myawesomeapp.com',
fromName: 'myname@mycompany.com',
}),
});
// => From: "myname@mycompany.com" <noreply@myawesomeapp.com>
```

### Device Tracking

User pools can be configured to track devices that users have logged in to.
Expand Down
63 changes: 60 additions & 3 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts
Expand Up @@ -164,9 +164,10 @@ class SESEmail extends UserPoolEmail {
throw new Error('Your stack region cannot be determined so "sesRegion" is required in SESOptions');
}

let from = this.options.fromEmail;
let from = encodeAndTest(this.options.fromEmail);
if (this.options.fromName) {
from = `${this.options.fromName} <${this.options.fromEmail}>`;
const fromName = formatFromName(this.options.fromName);
from = `${fromName} <${from}>`;
}

if (this.options.sesVerifiedDomain) {
Expand All @@ -177,7 +178,7 @@ class SESEmail extends UserPoolEmail {
}

return {
from: encodeAndTest(from),
from,
replyToEmailAddress: encodeAndTest(this.options.replyTo),
configurationSet: this.options.configurationSetName,
emailSendingAccount: 'DEVELOPER',
Expand All @@ -202,3 +203,59 @@ function encodeAndTest(input: string | undefined): string | undefined {
return undefined;
}
}

/**
* Formats `fromName` to comply RFC 5322
*
* @see https://www.rfc-editor.org/rfc/rfc5322#section-3.4
*/
function formatFromName(fromName: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks this is much easier to follow! The only question I have is how this
handles different types of quotes? Will it still work if you use " or ```

fromName: "'foo.bar'"
fromName: `'${someVar}'`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per RFC 5322, both the single quote ' and the backquote ` are "atext". They are same as alphabet/digit letters. But surprising, the dot . is a "specials".

   atext           =   ALPHA / DIGIT /    ; Printable US-ASCII
                       "!" / "#" /        ;  characters not including
                       "$" / "%" /        ;  specials.  Used for atoms.
                       "&" / "'" /
                       "*" / "+" /
                       "-" / "/" /
                       "=" / "?" /
                       "^" / "_" /
                       "`" / "{" /
                       "|" / "}" /
                       "~"

Note: White space is not defined as "atext", but also allowed.

Examples:

source template cause
"'foo.bar'" quoted-string
"From": "\"'foo.bar'\" <addr@example.com>"
the dot . is "specials"
"'foo bar'" bare
"From": "'foo bar' <addr@example.com>"
all chars are "atext"
"Foo's Bar" bare
"From": "Foo's Bar <addr@example.com>"
all chars are "atext"
'${price}' bare
"From": "${price} <addr@example.com>"
all chars are "atext"
"`foo bar`" bare
"From": "`foo bar` <addr@example.com>"
all chars are "atext"
'Café' mime encode
"From": "=?UTF-8?B?Q2Fmw6k=?= <addr@example.com>"
é is not US-ASCII
'"Café"' entire "Café" including double quote " is mime encoded
"From": "=?UTF-8?B?IkNhZsOpIg==?= <addr@example.com>"
é is not US-ASCII
`'${someVar}'` mime encode if someVar is not US-ASCII
quoted-string if someVar includes "specials"
otherwise, as-is
`~` is evaluated at runtime
single quote ' is atext
`"${someVar}"` mime encode if someVar is not US-ASCII
quoted-string again if someVar includes malformed escape
otherwise, as-is
already quoted-string form

// mime encode for non US-ASCII characters
// see RFC 2047 for details https://www.rfc-editor.org/rfc/rfc2047
if (!isAscii(fromName)) {
const base64Name = Buffer.from(fromName, 'utf-8').toString('base64');
return `=?UTF-8?B?${base64Name}?=`;
}

// makes a quoted-string unless fromName is a phrase (only atext and space)
// or a quoted-string already
if (!(isSimplePhrase(fromName) || isQuotedString(fromName))) {
// in quoted-string, `\` and `"` should be escaped by `\`
// e.g. `"foo \"bar\" \\baz"`
const quotedName = fromName.replace(/[\\"]/g, (ch) => `\\${ch}`);
return `"${quotedName}"`;
}

// otherwise, returns as is
return fromName;
}

/**
* Returns whether the input is a printable US-ASCII string
*/
function isAscii(input: string): boolean {
// U+0020 (space) - U+007E (`~`)
return /^[\u0020-\u007E]+$/u.test(input);
}

/**
* Returns whether the input is a phrase excluding quoted-string
*
* @see https://www.rfc-editor.org/rfc/rfc5322#section-3.2
*/
function isSimplePhrase(input: string): boolean {
return /^[\w !#$%&'*+-\/=?^_`{|}~]+$/.test(input);
}

/**
* Returns whether the input is already a quoted-string
*
* @see https://www.rfc-editor.org/rfc/rfc5322#section-3.2.4
*/
function isQuotedString(input: string): boolean {
// in quoted-string, `\` and `"` should be esacaped by `\`
//
// match: `"foo.bar"` / `"foo \"bar\""` / `"foo \\ bar"`
// not match: `"bare " dquote"` / `"unclosed escape \"` / `"unclosed dquote`
return /^"(?:[^\\"]|\\.)*"$/.test(input);
}
@@ -0,0 +1,19 @@
{
"version": "22.0.0",
"files": {
"21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
"source": {
"path": "IntegTestDefaultTestDeployAssertE3E7D2A4.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
}
},
"dockerImages": {}
}
@@ -0,0 +1,36 @@
{
"Parameters": {
"BootstrapVersion": {
"Type": "AWS::SSM::Parameter::Value<String>",
"Default": "/cdk-bootstrap/hnb659fds/version",
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
}
},
"Rules": {
"CheckBootstrapVersion": {
"Assertions": [
{
"Assert": {
"Fn::Not": [
{
"Fn::Contains": [
[
"1",
"2",
"3",
"4",
"5"
],
{
"Ref": "BootstrapVersion"
}
]
}
]
},
"AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
}
]
}
}
}
@@ -1 +1 @@
{"version":"20.0.0"}
{"version":"22.0.0"}
@@ -1,15 +1,15 @@
{
"version": "20.0.0",
"version": "22.0.0",
"files": {
"3294a2beef1e4a711276251bf311cdf22b70152f30241f0d155898e2ab9ad091": {
"55384e618066ba251d0576ca224e2109d90f1e97e067d2d9bfb1476d43fff838": {
"source": {
"path": "integ-user-pool-signup-code.template.json",
"path": "integ-user-ses-email.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "3294a2beef1e4a711276251bf311cdf22b70152f30241f0d155898e2ab9ad091.json",
"objectKey": "55384e618066ba251d0576ca224e2109d90f1e97e067d2d9bfb1476d43fff838.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
Expand Down
Expand Up @@ -20,7 +20,7 @@
},
"EmailConfiguration": {
"EmailSendingAccount": "DEVELOPER",
"From": "noreply@example.com",
"From": "\"myname@mycompany.com\" <noreply@example.com>",
"ReplyToEmailAddress": "support@example.com",
"SourceArn": {
"Fn::Join": [
Expand Down
@@ -1,14 +1,12 @@
{
"version": "20.0.0",
"version": "22.0.0",
"testCases": {
"integ.user-pool-ses-email": {
"IntegTest/DefaultTest": {
"stacks": [
"integ-user-pool-signup-code"
"integ-user-ses-email"
],
"diffAssets": false,
"stackUpdateWorkflow": true
"assertionStack": "IntegTest/DefaultTest/DeployAssert",
"assertionStackName": "IntegTestDefaultTestDeployAssertE3E7D2A4"
}
},
"synthContext": {},
"enableLookups": false
}
}
@@ -1,33 +1,27 @@
{
"version": "20.0.0",
"version": "22.0.0",
"artifacts": {
"Tree": {
"type": "cdk:tree",
"properties": {
"file": "tree.json"
}
},
"integ-user-pool-signup-code.assets": {
"integ-user-ses-email.assets": {
"type": "cdk:asset-manifest",
"properties": {
"file": "integ-user-pool-signup-code.assets.json",
"file": "integ-user-ses-email.assets.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version"
}
},
"integ-user-pool-signup-code": {
"integ-user-ses-email": {
"type": "aws:cloudformation:stack",
"environment": "aws://unknown-account/unknown-region",
"properties": {
"templateFile": "integ-user-pool-signup-code.template.json",
"templateFile": "integ-user-ses-email.template.json",
"validateOnSynth": false,
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/3294a2beef1e4a711276251bf311cdf22b70152f30241f0d155898e2ab9ad091.json",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/55384e618066ba251d0576ca224e2109d90f1e97e067d2d9bfb1476d43fff838.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
"additionalDependencies": [
"integ-user-pool-signup-code.assets"
"integ-user-ses-email.assets"
],
"lookupRole": {
"arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}",
Expand All @@ -36,35 +30,88 @@
}
},
"dependencies": [
"integ-user-pool-signup-code.assets"
"integ-user-ses-email.assets"
],
"metadata": {
"/integ-user-pool-signup-code/myuserpool/Resource": [
"/integ-user-ses-email/myuserpool/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "myuserpool01998219"
}
],
"/integ-user-pool-signup-code/user-pool-id": [
"/integ-user-ses-email/user-pool-id": [
{
"type": "aws:cdk:logicalId",
"data": "userpoolid"
}
],
"/integ-user-pool-signup-code/BootstrapVersion": [
"/integ-user-ses-email/BootstrapVersion": [
{
"type": "aws:cdk:logicalId",
"data": "BootstrapVersion"
}
],
"/integ-user-pool-signup-code/CheckBootstrapVersion": [
"/integ-user-ses-email/CheckBootstrapVersion": [
{
"type": "aws:cdk:logicalId",
"data": "CheckBootstrapVersion"
}
]
},
"displayName": "integ-user-pool-signup-code"
"displayName": "integ-user-ses-email"
},
"IntegTestDefaultTestDeployAssertE3E7D2A4.assets": {
"type": "cdk:asset-manifest",
"properties": {
"file": "IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version"
}
},
"IntegTestDefaultTestDeployAssertE3E7D2A4": {
"type": "aws:cloudformation:stack",
"environment": "aws://unknown-account/unknown-region",
"properties": {
"templateFile": "IntegTestDefaultTestDeployAssertE3E7D2A4.template.json",
"validateOnSynth": false,
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
"cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
"additionalDependencies": [
"IntegTestDefaultTestDeployAssertE3E7D2A4.assets"
],
"lookupRole": {
"arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}",
"requiresBootstrapStackVersion": 8,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version"
}
},
"dependencies": [
"IntegTestDefaultTestDeployAssertE3E7D2A4.assets"
],
"metadata": {
"/IntegTest/DefaultTest/DeployAssert/BootstrapVersion": [
{
"type": "aws:cdk:logicalId",
"data": "BootstrapVersion"
}
],
"/IntegTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [
{
"type": "aws:cdk:logicalId",
"data": "CheckBootstrapVersion"
}
]
},
"displayName": "IntegTest/DefaultTest/DeployAssert"
},
"Tree": {
"type": "cdk:tree",
"properties": {
"file": "tree.json"
}
}
}
}