-
Notifications
You must be signed in to change notification settings - Fork 634
Description
Checkboxes for prior research
- I've gone through Developer Guide and API reference
- I've checked AWS Forums and StackOverflow.
- I've searched for previous similar issues and didn't find any solution.
Describe the bug
I am working on implementing presigned post urls for .net sdk and i noticed something while testing the javascript sdk.
When using ${filename} variable in the success_action_redirect field with createPresignedPost(), S3 rejects the upload with AccessDenied error. The issue is that the JavaScript SDK adds an exact match condition for the success_action_redirect field (without replacing ${filename} with the actual filename, but S3 tries to substitute ${filename} with the actual filename during upload, causing a policy condition mismatch.
If you check the repro steps below, everything works when the success_action_redirect
is some regular text. but as soon as i include ${filename}
at the end of it, it fails, which suggests s3 is replacing that on the backend with the actual file name. But the issue javascript sdk isnt updating the condition before sending it
Regression Issue
- Select this option if this issue appears to be a regression.
SDK version number
"@aws-sdk/client-s3": "^3.445.0", "@aws-sdk/s3-presigned-post": "^3.445.0", "node-fetch": "^3.3.2", "form-data": "^4.0.0"
Which JavaScript Runtime is this issue in?
Node.js
Details of the browser/Node.js/ReactNative version
v22.14.0
Reproduction Steps
import {
S3Client,
CreateBucketCommand,
ListObjectsV2Command,
DeleteObjectsCommand,
DeleteBucketCommand,
HeadBucketCommand,
waitUntilBucketExists
} from '@aws-sdk/client-s3';
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
import fetch from 'node-fetch';
import FormData from 'form-data';
// Configuration
const REGION = 'us-west-2';
const BUCKET_NAME = `s3-filename-variable-test-${Date.now()}`;
const s3Client = new S3Client({ region: REGION });
async function setupBucket() {
try {
console.log(`Setting up test bucket: ${BUCKET_NAME}`);
// Check if bucket already exists
try {
await s3Client.send(new HeadBucketCommand({ Bucket: BUCKET_NAME }));
console.log(`Bucket ${BUCKET_NAME} already exists, using it for tests`);
} catch (err) {
// Create bucket if it doesn't exist
console.log(`Creating bucket ${BUCKET_NAME}...`);
await s3Client.send(new CreateBucketCommand({
Bucket: BUCKET_NAME,
CreateBucketConfiguration: {
LocationConstraint: REGION !== 'us-east-1' ? REGION : undefined
}
}));
// Wait for bucket to be available
await waitUntilBucketExists(
{ client: s3Client, maxWaitTime: 60 },
{ Bucket: BUCKET_NAME }
);
console.log(`Bucket ${BUCKET_NAME} created successfully`);
}
return true;
} catch (err) {
console.error('Error setting up bucket:', err);
return false;
}
}
async function cleanupBucket() {
try {
console.log(`\nCleaning up bucket: ${BUCKET_NAME}`);
// List all objects in the bucket
const listResult = await s3Client.send(new ListObjectsV2Command({
Bucket: BUCKET_NAME
}));
if (listResult.Contents && listResult.Contents.length > 0) {
console.log(`Found ${listResult.Contents.length} objects to delete`);
// Delete all objects
await s3Client.send(new DeleteObjectsCommand({
Bucket: BUCKET_NAME,
Delete: {
Objects: listResult.Contents.map(obj => ({ Key: obj.Key }))
}
}));
console.log('All objects deleted');
} else {
console.log('No objects to delete');
}
// Delete the bucket
await s3Client.send(new DeleteBucketCommand({
Bucket: BUCKET_NAME
}));
console.log(`Bucket ${BUCKET_NAME} deleted successfully`);
return true;
} catch (err) {
console.error('Error during cleanup:', err);
return false;
}
}
async function testRedirect(redirectUrl) {
try {
console.log(`\nTesting with redirect URL: "${redirectUrl}"`);
// Create presigned POST with success_action_redirect
const { url, fields } = await createPresignedPost(s3Client, {
Bucket: BUCKET_NAME,
Key: `test-${Date.now()}.txt`,
Fields: {
'Content-Type': 'text/plain',
'success_action_redirect': redirectUrl
},
Conditions: [
['content-length-range', 1, 1048576]
],
Expires: 600
});
// Decode policy to see conditions
const policyBase64 = fields.Policy;
const policyJson = Buffer.from(policyBase64, 'base64').toString('utf-8');
const policy = JSON.parse(policyJson);
console.log('\nGenerated policy conditions:');
policy.conditions.forEach(condition => {
console.log(JSON.stringify(condition));
});
// Find the success_action_redirect condition
const redirectCondition = policy.conditions.find(
c => typeof c === 'object' && c['success_action_redirect'] !== undefined
);
console.log('\nSuccess_action_redirect condition:',
redirectCondition ? JSON.stringify(redirectCondition) : 'None found');
// Try uploading
console.log('\nAttempting upload...');
const form = new FormData();
// Add all fields from the presigned POST response
Object.entries(fields).forEach(([key, value]) => {
form.append(key, value);
});
// Add file content
const testContent = 'Test content for filename variable bug reproduction';
form.append('file', Buffer.from(testContent), {
filename: 'test.txt',
contentType: 'text/plain'
});
// Perform the upload
const response = await fetch(url, {
method: 'POST',
body: form
});
console.log(`Upload status: ${response.status}`);
if (!response.ok) {
const text = await response.text();
console.log(`Error response: ${text}`);
return false;
} else {
console.log('Upload successful!');
return true;
}
} catch (err) {
console.error('Error during test:', err);
return false;
}
}
async function main() {
try {
console.log('S3 ${filename} Variable Bug Reproduction');
console.log('======================================\n');
// Setup the test bucket
const setupSuccess = await setupBucket();
if (!setupSuccess) {
console.error('Failed to set up test bucket, aborting tests');
return;
}
// Test 1: With ${filename} in success_action_redirect (will fail)
console.log('\n== TEST 1: With ${filename} in success_action_redirect ==');
const test1Result = await testRedirect('https://example.com/success?file=${filename}');
console.log(`\nTEST 1 RESULT: ${test1Result ? 'PASSED' : 'FAILED'}`);
if (test1Result) {
console.log('Note: This test was expected to fail but passed. The issue may have been fixed.');
} else {
console.log('Note: This test failed as expected, confirming the issue exists.');
}
// Test 2: Without ${filename} (control test - should work)
console.log('\n== TEST 2: Without ${filename} (control test) ==');
const test2Result = await testRedirect('https://example.com/success');
console.log(`\nTEST 2 RESULT: ${test2Result ? 'PASSED' : 'FAILED'}`);
if (!test2Result) {
console.log('Warning: Control test failed. There may be other issues with your setup.');
}
// Clean up resources
await cleanupBucket();
console.log('\nTest completed. If Test 1 failed and Test 2 passed, the issue is reproduced.');
} catch (err) {
console.error('Unexpected error:', err);
}
}
// Run the test
main().catch(console.error);
Observed Behavior
the ${filename}
is not replaced by the key
value when ${filename}
is included in one of the fields.
Expected Behavior
The AWS JavaScript SDK should detect ${filename} in any field (including success_action_redirect) and avoid generating exact match conditions that would interfere with S3's variable substitution. Theres currently logic that does this with the key https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/src/createPresignedPost.ts#L82 but im pretty sure it should be applied everywhere
Possible Solution
update https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-presigned-post/src/createPresignedPost.ts#L82 to replace for all fields instead of just the key
Additional Information/Context
No response